Skip to content

Commit c99023d

Browse files
authored
doc(flags): Document local eval in distributed envs (#14136)
* doc(flags): Document local eval in distributed envs This change expands upon the Node.js external caching guide to provide general information regarding Node.js and Python clients in distributed / stateless environments. * doc: Drop redundant note * docs: Fix missing link
1 parent c01de01 commit c99023d

File tree

11 files changed

+243
-136
lines changed

11 files changed

+243
-136
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Common patterns
2+
3+
### Shared caches with locking
4+
5+
When running multiple server instances with a shared cache like Redis, coordinate fetching so only one instance polls PostHog at a time.
6+
7+
The recommended pattern:
8+
9+
- One instance owns the lock for its entire lifetime, not just during a single fetch
10+
- Refresh the lock TTL each polling cycle to maintain ownership
11+
- Release on shutdown, but only if you own the lock
12+
- Let locks expire if a process crashes, so another instance can take over
13+
14+
#### Redis example
15+
16+
A complete working example written in Python using Redis with distributed locking is available in the [posthog-python repository](https://github.com/PostHog/posthog-python/blob/master/examples/redis_flag_cache.py). It implements the locking pattern described above.
17+
18+
### Caches without locking
19+
20+
Some storage backends like Cloudflare KV don't support atomic locking operations. In these cases, use a split read/write pattern:
21+
22+
1. A scheduled job (cron) periodically fetches flag definitions and writes to the cache
23+
2. Request handlers read from the cache and evaluate flags locally, with no API calls
24+
25+
This separates the concerns entirely. One process writes, all others read.
26+
27+
#### Cloudflare Workers example
28+
29+
A complete working example written in TypeScript is available in the [posthog-js repository](https://github.com/PostHog/posthog-js/tree/main/examples/example-cloudflare-kv-cache). It uses the split read/write pattern described above. The worker's scheduled job writes flag definitions to KV, and request handlers read from it.
30+
31+
This pattern is ideal for high-traffic edge applications where flag evaluation must be extremely fast and you can tolerate flag updates being slightly delayed.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
When using [local evaluation](/docs/feature-flags/local-evaluation), the SDK fetches feature flag definitions and stores them in memory. This works well for single-instance applications, but in distributed or stateless environments (multiple servers, edge workers, lambdas), each instance fetches its own copy, wasting API calls and adding latency on cold starts.
2+
3+
An **external cache provider** lets you store flag definitions in shared storage (Redis, database, Cloudflare KV, etc.) so all instances can read from a single source.
4+
5+
This enables you to:
6+
7+
- Share flag definitions across workers to reduce API calls
8+
- Coordinate fetching so only one worker polls at a time
9+
- Pre-cache definitions for ultra-low-latency flag evaluation
10+
11+
> **Note:** External cache providers are currently available in Node.js and Python SDKs only. This feature is experimental and may change in minor versions.
12+
13+
## When to use an external cache
14+
15+
| Scenario | Recommendation |
16+
| :------- | :------------- |
17+
| Single server instance | SDK's built-in memory cache is sufficient |
18+
| Multiple workers (same process) | SDK's built-in memory cache is sufficient |
19+
| Multiple servers/containers | Use Redis or database caching with distributed locks |
20+
| Edge workers (Cloudflare, Vercel Edge) | Use KV storage with split read/write pattern |
21+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
## Installation
2+
3+
Import the interface from the SDK:
4+
5+
```typescript
6+
import { FlagDefinitionCacheProvider, FlagDefinitionCacheData } from 'posthog-node/experimental'
7+
```
8+
9+
## The interface
10+
11+
To create a custom cache, implement the `FlagDefinitionCacheProvider` interface:
12+
13+
```typescript
14+
interface FlagDefinitionCacheProvider {
15+
// Retrieve cached flag definitions
16+
getFlagDefinitions(): Promise<FlagDefinitionCacheData | undefined> | FlagDefinitionCacheData | undefined
17+
18+
// Determine if this instance should fetch new definitions
19+
shouldFetchFlagDefinitions(): Promise<boolean> | boolean
20+
21+
// Store definitions after a successful fetch
22+
onFlagDefinitionsReceived(data: FlagDefinitionCacheData): Promise<void> | void
23+
24+
// Clean up resources on shutdown
25+
shutdown(): Promise<void> | void
26+
}
27+
```
28+
29+
When the SDK fetches flag definitions from the API, it passes a `FlagDefinitionCacheData` object to `onFlagDefinitionsReceived()` for you to store:
30+
31+
```typescript
32+
interface FlagDefinitionCacheData {
33+
flags: PostHogFeatureFlag[] // Feature flag definitions
34+
groupTypeMapping: Record<string, string> // Group type index to name mapping
35+
cohorts: Record<string, PropertyGroup> // Cohort definitions for local evaluation
36+
}
37+
```
38+
39+
### Method details
40+
41+
| Method | Purpose | Return value |
42+
| :----- | :------ | :----------- |
43+
| `getFlagDefinitions()` | Retrieve cached definitions. Called when the poller refreshes. | Cached data, or `undefined` if cache is empty |
44+
| `shouldFetchFlagDefinitions()` | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | `true` to fetch, `false` to skip |
45+
| `onFlagDefinitionsReceived(data)` | Store definitions after a successful API fetch. | void |
46+
| `shutdown()` | Release locks, close connections, clean up resources. | void |
47+
48+
> **Note:** All methods may throw errors. The SDK catches and logs them gracefully, ensuring cache provider errors never break flag evaluation.
49+
50+
## Using your cache provider
51+
52+
Pass your cache provider when initializing PostHog:
53+
54+
```typescript
55+
import { PostHog } from 'posthog-node'
56+
57+
const cache = new YourCacheProvider()
58+
59+
const posthog = new PostHog('<ph_project_api_key>', {
60+
personalApiKey: '<ph_personal_api_key>',
61+
enableLocalEvaluation: true,
62+
flagDefinitionCacheProvider: cache,
63+
})
64+
```
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
## Installation
2+
3+
Import the interface from the SDK:
4+
5+
```python
6+
from posthog import FlagDefinitionCacheProvider, FlagDefinitionCacheData
7+
```
8+
9+
## The interface
10+
11+
To create a custom cache, implement the `FlagDefinitionCacheProvider` protocol:
12+
13+
```python
14+
class FlagDefinitionCacheProvider(Protocol):
15+
def get_flag_definitions(self) -> Optional[FlagDefinitionCacheData]:
16+
"""Retrieve cached flag definitions."""
17+
...
18+
19+
def should_fetch_flag_definitions(self) -> bool:
20+
"""Determine if this instance should fetch new definitions."""
21+
...
22+
23+
def on_flag_definitions_received(self, data: FlagDefinitionCacheData) -> None:
24+
"""Store definitions after a successful fetch."""
25+
...
26+
27+
def shutdown(self) -> None:
28+
"""Clean up resources on shutdown."""
29+
...
30+
```
31+
32+
When the SDK fetches flag definitions from the API, it passes a `FlagDefinitionCacheData` object to `on_flag_definitions_received()` for you to store:
33+
34+
```python
35+
class FlagDefinitionCacheData(TypedDict):
36+
flags: List[Dict[str, Any]] # Feature flag definitions
37+
group_type_mapping: Dict[str, str] # Group type index to name mapping
38+
cohorts: Dict[str, Any] # Cohort definitions for local evaluation
39+
```
40+
41+
### Method details
42+
43+
| Method | Purpose | Return value |
44+
| :----- | :------ | :----------- |
45+
| `get_flag_definitions()` | Retrieve cached definitions. Called when the poller refreshes. | Cached data, or `None` if cache is empty |
46+
| `should_fetch_flag_definitions()` | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | `True` to fetch, `False` to skip |
47+
| `on_flag_definitions_received(data)` | Store definitions after a successful API fetch. | None |
48+
| `shutdown()` | Release locks, close connections, clean up resources. | None |
49+
50+
> **Note:** All methods may throw errors. The SDK catches and logs them gracefully, ensuring cache provider errors never break flag evaluation.
51+
52+
## Using your cache provider
53+
54+
Pass your cache provider when initializing PostHog:
55+
56+
```python
57+
from posthog import Posthog
58+
59+
cache = YourCacheProvider()
60+
61+
posthog = Posthog(
62+
'<ph_project_api_key>',
63+
personal_api_key='<ph_personal_api_key>',
64+
flag_definition_cache_provider=cache,
65+
)
66+
```
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: Local evaluation in distributed or stateless environments
3+
sidebar: Docs
4+
showTitle: true
5+
availability:
6+
free: full
7+
selfServe: full
8+
enterprise: full
9+
---
10+
11+
import Tab from "components/Tab"
12+
import LocalEvalDistributedEnvIntro from "./_snippets/distributed-environments-intro.mdx"
13+
import LocalEvalDistributedEnvCommonPatterns from "./_snippets/distributed-environments-common-patterns.mdx"
14+
import LocalEvalDistributedEnvNodeContent from "./_snippets/distributed-environments-node.mdx"
15+
import LocalEvalDistributedEnvPythonContent from "./_snippets/distributed-environments-python.mdx"
16+
17+
<LocalEvalDistributedEnvIntro />
18+
19+
<Tab.Group tabs={['Node.js', 'Python']}>
20+
<Tab.List>
21+
<Tab>Node.js</Tab>
22+
<Tab>Python</Tab>
23+
</Tab.List>
24+
<Tab.Panels>
25+
<Tab.Panel>
26+
<LocalEvalDistributedEnvNodeContent />
27+
</Tab.Panel>
28+
<Tab.Panel>
29+
<LocalEvalDistributedEnvPythonContent />
30+
</Tab.Panel>
31+
</Tab.Panels>
32+
</Tab.Group>
33+
34+
<LocalEvalDistributedEnvCommonPatterns />

contents/docs/feature-flags/local-evaluation.mdx renamed to contents/docs/feature-flags/local-evaluation/index.mdx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ availability:
1010

1111
> **Note:** Local evaluation is only available in the [Node](/docs/libraries/node), [Ruby](/docs/libraries/ruby), [Go](/docs/libraries/go), [Python](/docs/libraries/python), [C#/.NET](/docs/libraries/dotnet), [PHP](/docs/libraries/php), and [Java](/docs/libraries/java) SDKs.
1212
13-
> **Note:** Do not use local evaluation in edge/lambda environments and stateless PHP applications, as per-request initialization causes performance issues and unnecessarily inflated costs. Use regular flag evaluation instead.
13+
> **Note:** In edge/lambda environments and stateless PHP applications, local evaluation with the default in-memory cache causes performance issues and inflated costs due to per-request initialization. For these environments, use an [external cache provider](/docs/feature-flags/local-evaluation/distributed-environments) to share flag definitions across requests, or use regular flag evaluation instead.
1414
1515
Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests.
1616

@@ -20,7 +20,7 @@ There are 3 steps to enable local evaluation:
2020

2121
## Step 1: Find your feature flags secure API key
2222

23-
import ObtainFeatureFlagsSecureAPIKey from "../integrate/_snippets/obtain-flags-secure-key.mdx"
23+
import ObtainFeatureFlagsSecureAPIKey from "../../integrate/_snippets/obtain-flags-secure-key.mdx"
2424

2525
<ObtainFeatureFlagsSecureAPIKey />
2626

@@ -30,21 +30,21 @@ When you initialize PostHog with your feature flags secure API key, PostHog will
3030

3131
By default, PostHog fetches these definitions every 30 seconds (or 5 minutes in the Go SDK). However, you can change this frequency by specifying a different value in the polling interval argument.
3232

33-
> **Note:** For billing purposes, we count the request to fetch the feature flag definitions as being equivalent to `10 flags` requests.
34-
>
35-
> This is because one of these requests can compute feature flags for hundreds or thousands of users. It ensures local evaluation is priced fairly while remaining the most cost-effective option (by far!).
33+
> **Note:** For billing purposes, we count the request to fetch the feature flag definitions as being equivalent to `10 flags` requests.
34+
>
35+
> This is because one of these requests can compute feature flags for hundreds or thousands of users. It ensures local evaluation is priced fairly while remaining the most cost-effective option (by far!).
3636
37-
import ConfigureFlagsSecureKey from "../integrate/_snippets/configure-flags-secure-key.mdx"
37+
import ConfigureFlagsSecureKey from "../../integrate/_snippets/configure-flags-secure-key.mdx"
3838

3939
<ConfigureFlagsSecureKey />
4040

4141
## Step 3: Evaluate your feature flag
4242

4343
To evaluate the feature flag, call any of the flag related methods, like `getFeatureFlag` or `getAllFlags`, as you normally would. The only difference is that you must provide any `person properties`, `groups` or `group properties` used to evaluate the [release conditions](/docs/feature-flags/creating-feature-flags#release-conditions) of the flag.
4444

45-
Then, by default, PostHog attempts to evaluate the flag locally using definitions it loads on initialization and at the `poll interval`. If this fails, PostHog then makes a server request to fetch the flag value.
45+
Then, by default, PostHog attempts to evaluate the flag locally using definitions it loads on initialization and at the `poll interval`. If this fails, PostHog then makes a server request to fetch the flag value.
4646

47-
You can disable this behavior by setting `onlyEvaluateLocally` to `true`. In this case, PostHog will **only** attempt to evaluate the flag locally, and return `undefined` / `None` / `nil` if it was unable to.
47+
You can disable this behavior by setting `onlyEvaluateLocally` to `true`. In this case, PostHog will **only** attempt to evaluate the flag locally, and return `undefined` / `None` / `nil` if it was unable to.
4848

4949
<MultiLanguage>
5050

@@ -67,7 +67,7 @@ await client.getFeatureFlag(
6767
},
6868
'another_group_type': {
6969
'group_property_name': 'another value'
70-
}
70+
}
7171
},
7272
onlyEvaluateLocally: false, // Optional. Defaults to false. Set to true if you don't want PostHog to make a server request if it can't evaluate locally
7373
}
@@ -92,7 +92,7 @@ posthog.get_feature_flag(
9292
}
9393
'another_group_type': {
9494
'group_property_name': 'another value'
95-
}
95+
}
9696
},
9797
only_evaluate_locally: False # Optional. Defaults to False. Set to True if you don't want PostHog to make a server request if it can't evaluate locally
9898
)
@@ -151,7 +151,7 @@ PostHog::getFeatureFlag(
151151
], // groups
152152
['property_name' => 'value'], // person properties
153153
[
154-
'your_group_type' => ['group_property_name' => 'value'],
154+
'your_group_type' => ['group_property_name' => 'value'],
155155
'another_group_type' => ['group_property_name' => 'another value']
156156
], // group properties
157157
false, // only_evaluate_locally, Optional. Defaults to false. Set to true if you don't want PostHog to make a server request if it can't evaluate locally

contents/docs/libraries/node/index.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ await client.reloadFeatureFlags()
247247
// Do something with feature flags here
248248
```
249249
250-
#### External caching
250+
#### Distributed environments
251251
252-
In multi-worker or edge environments, you can implement custom caching for flag definitions using Redis, Cloudflare KV, or other storage backends. This enables sharing definitions across workers and coordinating fetches. See the [external caching guide](/tutorials/node-external-cache) for details.
252+
In multi-worker or edge environments, you can implement custom caching for flag definitions using Redis, Cloudflare KV, or other storage backends. This enables sharing definitions across workers and coordinating fetches. See our guide for [local evaluation in distributed environments](/docs/feature-flags/local-evaluation/distributed-environments?tab=Node.js) for details.
253253
254254
## Experiments (A/B tests)
255255

contents/docs/libraries/python/index.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ import LocalEvaluationIntro from "../../feature-flags/snippets/local-evaluation-
131131

132132
For details on how to implement local evaluation, see our [local evaluation guide](/docs/feature-flags/local-evaluation).
133133

134+
#### Distributed environments
135+
136+
In multi-worker or edge environments, you can implement custom caching for flag definitions using Redis, Cloudflare KV, or other storage backends. This enables sharing definitions across workers and coordinating fetches. See our guide for [local evaluation in distributed environments](/docs/feature-flags/local-evaluation/distributed-environments?tab=Python) for details.
137+
134138
## Experiments (A/B tests)
135139

136140
Since [experiments](/docs/experiments/manual) use feature flags, the code for running an experiment is very similar to the feature flags code:

0 commit comments

Comments
 (0)