Local evaluation in distributed or stateless environments
When using 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.
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.
This enables you to:
- Share flag definitions across workers to reduce API calls
- Coordinate fetching so only one worker polls at a time
- Pre-cache definitions for ultra-low-latency flag evaluation
Note: External cache providers are currently available in the Node.js, Python, Ruby, and JVM (Java) SDKs.
When to use an external cache
| Scenario | Recommendation |
|---|---|
| Single server instance | SDK's built-in memory cache is sufficient |
| Multiple workers (same process) | SDK's built-in memory cache is sufficient |
| Multiple servers/containers | Use Redis or database caching with distributed locks |
| Edge workers (Cloudflare, Vercel Edge) | Use KV storage with split read/write pattern |
Installation
Import the interface from the SDK:
The interface
To create a custom cache, implement the FlagDefinitionCacheProvider interface:
When the SDK fetches flag definitions from the API, it passes a FlagDefinitionCacheData object to onFlagDefinitionsReceived() for you to store:
Method details
| Method | Purpose | Return value |
|---|---|---|
getFlagDefinitions() | Retrieve cached definitions. Called when the poller refreshes. | Cached data, or undefined if cache is empty |
shouldFetchFlagDefinitions() | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | true to fetch, false to skip |
onFlagDefinitionsReceived(data) | Store definitions after a successful API fetch. | void |
shutdown() | Release locks, close connections, clean up resources. | void |
Note: All methods may throw errors. The SDK catches and logs them gracefully, ensuring cache provider errors never break flag evaluation.
Using your cache provider
Pass your cache provider when initializing PostHog:
Installation
Import the interface from the SDK:
The interface
To create a custom cache, implement the FlagDefinitionCacheProvider protocol:
You can implement these methods as either sync or async methods. If a method is async, the SDK waits for it to finish before continuing.
Note: If you use an async client that is tied to an event loop, such as
redis.asyncio, use a client dedicated to this cache provider. The SDK runs async cache provider methods on its own background event loop.
When the SDK fetches flag definitions from the API, it passes a FlagDefinitionCacheData object to on_flag_definitions_received() for you to store:
Method details
| Method | Purpose | Return value |
|---|---|---|
get_flag_definitions() | Retrieve cached definitions. Called when the poller refreshes. | Cached data or None; async implementations return the same after being awaited |
should_fetch_flag_definitions() | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | True to fetch or False to skip; async implementations return the same after being awaited |
on_flag_definitions_received(data) | Store definitions after a successful API fetch. | None; async implementations return None after being awaited |
shutdown() | Release locks, close connections, clean up resources. | None; async implementations return None after being awaited |
Note: It is safe for cache provider implementations to raise exceptions. The SDK catches provider exceptions and continues without re-raising them:
should_fetch_flag_definitions()falls back to fetching,get_flag_definitions()falls back to the API, andon_flag_definitions_received()orshutdown()failures do not stop flag evaluation or shutdown.
Using your cache provider
Pass your cache provider when initializing PostHog:
Installation
External flag definition caches are available in the Ruby SDK. No additional PostHog package is required.
Import the SDK and pass a cache provider object when you initialize the client:
The interface
The Ruby SDK accepts any object that responds to the required cache provider methods. The provider is validated when you pass it as flag_definition_cache_provider.
When the SDK fetches flag definitions from the API, it passes a hash to on_flag_definitions_received(data) for you to store. Return the same shape from flag_definitions:
flags: Feature flag definitionsgroup_type_mapping: Group type index to name mappingcohorts: Cohort definitions for local evaluation
The SDK accepts either symbol or string keys for this hash, so returning data parsed from JSON is supported.
Method details
| Method | Purpose | Return value |
|---|---|---|
flag_definitions | Retrieve cached definitions. Called when the poller refreshes and should_fetch_flag_definitions? returns false. | Cached data, or nil if cache is empty |
should_fetch_flag_definitions? | Decide if this process should fetch. Use for distributed coordination (e.g., locks). | true to fetch from PostHog and store the result, false to read from cache |
on_flag_definitions_received(data) | Store definitions after a successful API fetch. | void |
shutdown | Release locks, close connections, and clean up resources. | void |
Note: Provider methods may raise errors. The SDK catches and logs provider errors: failed
should_fetch_flag_definitions?calls fall back to fetching from PostHog, failed cache reads fall back to the API, and failed writes or shutdowns do not stop flag evaluation or shutdown. Ifflag_definitionsreturnsnilbefore the first successful load, the SDK fetches from PostHog; if definitions are already loaded, it keeps the in-memory definitions.
The provider is used by local evaluation polling, so configure personal_api_key. Without it, the Ruby SDK disables local evaluation and doesn't poll flag definitions. Provider methods can run during client initialization and from the background polling task, so implementations should be safe to call repeatedly. If your cache uses TTLs, use a TTL longer than the polling interval; on_flag_definitions_received(data) is called only after modified API responses, not after 304 Not Modified responses.
Using your cache provider
Pass your cache provider when initializing PostHog:
Installation
External flag definition caches are available in posthog-server 2.7.0 and later.
Import the cache provider types from the SDK:
The interface
The JVM SDK exposes an async-capable PostHogFlagDefinitionCacheProvider interface. If your cache client is synchronous, extend PostHogBlockingFlagDefinitionCacheProvider instead.
For blocking clients, implement the blocking base class:
When the SDK fetches flag definitions from the API, it passes a JSON-compatible Map<String, Object> to onFlagDefinitionsReceived() for you to store. Return the same shape from getFlagDefinitions():
flags: Feature flag definitionsgroup_type_mapping: Group type index to name mappingcohorts: Cohort definitions for local evaluation
Method details
| Method | Purpose | Return value |
|---|---|---|
getFlagDefinitions() / getFlagDefinitionsBlocking() | Retrieve cached definitions. Called when the poller refreshes and shouldFetchFlagDefinitions() returns false. | Cached data, or null if cache is empty |
shouldFetchFlagDefinitions() / shouldFetchFlagDefinitionsBlocking() | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | true to fetch from PostHog and store the result, false to read from cache |
onFlagDefinitionsReceived(data) / onFlagDefinitionsReceivedBlocking(data) | Store definitions after a successful API fetch. | void |
shutdown() / shutdownBlocking() | Release locks, close connections, and clean up resources. | void |
Note: Provider methods may throw errors or complete exceptionally. The SDK catches and logs provider errors: failed
shouldFetchFlagDefinitions()calls fall back to fetching from PostHog, failed cache reads fall back to the API when no definitions are loaded, and failed writes or shutdowns do not stop flag evaluation. Async provider calls are waited on by the SDK and time out after 10 seconds.
Using your cache provider
Pass your cache provider when initializing PostHog:
Common patterns
Shared caches with locking
When running multiple server instances with a shared cache like Redis, coordinate fetching so only one instance polls PostHog at a time.
The recommended pattern:
- One instance owns the lock for its entire lifetime, not just during a single fetch
- Refresh the lock TTL each polling cycle to maintain ownership
- Release on shutdown, but only if you own the lock
- Let locks expire if a process crashes, so another instance can take over
Redis example
Complete working examples written in Python using Redis with distributed locking are available in the PostHog Python repository:
- Synchronous Redis cache provider
- Async Redis cache provider for async-first applications using
redis.asyncio
They implement the locking pattern described above.
Caches without locking
Some storage backends like Cloudflare KV don't support atomic locking operations. In these cases, use a split read/write pattern:
- A scheduled job (cron) periodically fetches flag definitions and writes to the cache
- Request handlers read from the cache and evaluate flags locally, with no API calls
This separates the concerns entirely. One process writes, all others read.
Cloudflare Workers example
A complete working example written in TypeScript is available in the posthog-js repository. 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.
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.