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 to share flag definitions across requests, or use regular flag evaluation instead.
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.
It is best practice to use local evaluation flags when possible, since this enables you to resolve flags faster and with fewer API calls.
How local evaluation works
To understand local evaluation, it helps to see how remote evaluation (the default) works first.
With remote evaluation, your server sends a request to PostHog's /flags API every time you check a flag. PostHog looks up all the data it needs — flag definitions, person properties, group properties, and cohort memberships — from its own database, evaluates the flag, and returns the result:
sequenceDiagram
participant App as Your app
participant SDK as PostHog SDK
participant PH as PostHog
App->>SDK: flag key + user ID
SDK->>PH: POST /flags
Note right of PH: Resolves using stored flag definitions,<br/>person properties, group properties,<br/>and cohort memberships
PH-->>SDK: true / false / variant
SDK-->>App: true / false / variant
Every flag check is a network round trip. PostHog already has all the data it needs, so you just pass the flag key and a user ID.
With local evaluation, the SDK periodically fetches flag definitions from PostHog's /flags/definitions endpoint in the background. When you check a flag, you provide the properties and the SDK evaluates locally — no round trip:
sequenceDiagram
participant App as Your app
participant SDK as PostHog SDK
participant PH as PostHog
Note over SDK,PH: Background (every 30s)
SDK->>PH: GET /flags/definitions
PH-->>SDK: Flag definitions
Note over App,SDK: At evaluation time
App->>SDK: flag key + person properties + group properties
Note right of SDK: Evaluates locally using<br/>cached flag definitions +<br/>properties you provided
SDK-->>App: true / false / variant
Notice that PostHog is not involved at evaluation time. The tradeoff: you are responsible for providing every property the flag's release conditions depend on. If you forget to pass a property, the SDK can't evaluate the flag locally and will either fall back to a remote request or return undefined (depending on your configuration).
Local evaluation is also more cost-effective: with remote evaluation, every flag check is a billable request, whereas local evaluation only bills for the periodic request to fetch flag definitions (counted as 10 flag requests each). For high-traffic services, this can significantly reduce your flag usage costs.
There are 3 steps to enable local evaluation:
Step 1: Get a secret API key
Local evaluation requires a secret API key that gives the SDK access to the feature flag definitions for your project. This key needs to be kept secret, and should not be used in the frontend or exposed to users.
We recommend using a project secret API key with the feature_flag:read scope. Project secret API keys are hashed at rest, scoped to only what they need, and you can create up to 50 per project, so you can issue and roll keys per deployment without affecting the others.
Note: Existing feature flags secure API keys and personal API keys continue to work for local evaluation, but both are deprecated. We recommend switching to a project secret API key.
Create a new key with the Local feature flag evaluation preset (the feature_flag:read scope) and give it a label.
Copy the key. It's only displayed once, so store it somewhere safe, like a secrets manager.
Use it to initialize the PostHog client.
Step 2: Initialize PostHog with your secret API key
When you initialize PostHog with your secret API key, PostHog will use the key to automatically fetch feature flag definitions. These definitions are then used to evaluate feature flags locally.
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.
Note: For billing purposes, we count the request to fetch the feature flag definitions as being equivalent to 10 flags requests.
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!).
const client =newPostHog(
'<ph_project_token>',
{
host:'https://us.i.posthog.com',
personalApiKey:'your secret API key from step 1',
featureFlagsPollingInterval:30000// Optional. Measured in milliseconds. Defaults to 30000 (30 seconds)
}
)
posthog = PostHog::Client.new({
api_key:"<ph_project_token>",
host:"https://us.i.posthog.com",
personal_api_key:'your secret API key from step 1',
feature_flags_polling_interval:30# Optional. Measured in seconds. Defaults to 30.
})
package main
import(
"os"
"time"
"github.com/posthog/posthog-go"
)
funcmain(){
client,_:= posthog.NewWithConfig(
os.Getenv("POSTHOG_API_KEY"),
posthog.Config{
Endpoint:"https://us.i.posthog.com",
PersonalApiKey:"your secret API key",// Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower.
DefaultFeatureFlagsPollingInterval: time.Minute *5,// time.Duration // Optional. Defaults to 5 minutes.
// NextFeatureFlagsPollingTick: func() time.Duration {} // Optional. Use this to sync polling intervals between instances. For an example see: https://github.com/PostHog/posthog-go/pull/36#issuecomment-1991734125
},
)
defer client.Close()
// run commands
}
PostHog::init(
'<ph_project_token>',
['host'=>'https://us.i.posthog.com'],
personalAPIKey:'your secret API key from step 1'
);
// NOTE: PHP currently doesn't support a poller, so you'll need to reload flags as and when needed, or re-initialize the posthog client
from posthog import Posthog
posthog = Posthog('<ph_project_token>',
host='https://us.i.posthog.com',
poll_interval=30,# Optional. Measured in seconds. Defaults to 30.
personal_api_key='your secret API key from step 1'
)
// Note: We don't recommend passing these in directly.
// Instead, rely on the config system and set the personal
.personalApiKey("your secret API key from step 1")
.pollIntervalSeconds(30)// Optional. Measured in seconds. Defaults to 30.
.build();
PostHogInterface posthog =PostHog.with(config);
useposthog_rs::ClientOptionsBuilder;
let options =ClientOptionsBuilder::default()
.api_key("<ph_project_token>")
.personal_api_key("your secret API key from step 1")
.enable_local_evaluation(true)
.poll_interval_seconds(30)// Optional. Measured in seconds. Defaults to 30.
.build()
.unwrap();
let client =posthog_rs::client(options).await;
Step 3: Evaluate your feature flag
To evaluate the feature flag, call the flag evaluation method for your SDK. In server-side APIs, evaluate flags once and read values from the returned snapshot. The only difference is that you must provide any person properties, groups, or group properties used to evaluate the release conditions of the flag.
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.
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.
Cold start and fallback defaults
When the SDK first starts, it needs up to one polling interval (30 seconds by default) to fetch flag definitions. During this window, local evaluation returns undefined / None because no definitions are loaded yet. Use per-flag defaults to control what users see during this period:
JavaScript
// Without a default, undefined is falsy — users see nothing
If you deploy frequently or have many short-lived workers, consider using a shared flag definition cache so definitions survive restarts and the cold start window is eliminated.
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.
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.
)
flag_value = flags.get_flag("flag-key")
$flags=PostHog::evaluateFlags(
distinctId:'distinct_id_of_the_user',
// Include any person properties, groups, or group properties required to evaluate the flag
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.
);
$flagValue=$flags->getFlag('flag-key');
var flags =await posthog.EvaluateFlagsAsync(
"distinct_id_of_the_user",
options:newAllFeatureFlagsOptions
{
PersonProperties =new()
{
["property_name"]="value",
},
Groups =new()
{
newGroup("your_group_type","your_group_id")
{
["group_property_name"]="value",
},
newGroup("another_group_type","another_group_id")
{
["group_property_name"]="another value",
},
},
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.
.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.
.build()
);
Object flagValue = flags.getFlag("flag-key");
usestd::collections::HashMap;
useposthog_rs::EvaluateFlagsOptions;
useserde_json::json;
// Include any person properties, groups, or group properties required to evaluate the flag
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.
..Default::default()
},
).await.unwrap();
let flag = flags.get_flag("flag-key");
Reloading flags
As mentioned in step 2, PostHog periodically fetches feature flag definitions. However, you can also force a reload by calling reloadFeatureFlags():
await client.reloadFeatureFlags();
posthog.reload_feature_flags()
client.ReloadFeatureFlags()
posthog.load_feature_flags()
PostHog::getClient()->loadFlags()
posthog.reloadFeatureFlags()
// The Rust SDK automatically reloads flags in the background
// at the interval specified by poll_interval_seconds (default: 30).
Use the is_not_set property operator, since local evaluation can only check properties you provide — it can't determine whether a property is absent on a person.
Have "Stop evaluation at first matching condition set" (early exit) enabled. Local evaluation currently always evaluates all condition groups, even when this setting is enabled. This is a temporary limitation while support is rolled out across SDKs. For now, the setting only takes effect when flags are evaluated through the /flags endpoint.
Dynamic cohort restrictions
Note: This restriction does not apply to our Go SDK, Rust SDK, v2.6.0 and above of our Node SDK, and to v2.4.0 and above of our Python SDK.
To enable local evaluation of feature flags that depend on dynamic cohorts, we translate the cohort definition into person properties.
However, there are a few constraints, and cohorts cannot be evaluated locally if:
There is a variant override on the condition with the cohort.
They have non-person properties.
There's more than one cohort in the feature flag definition.
The cohort in the feature flag is in the same group as another condition.
The cohort has nested AND-OR filters. Only simple cohorts that have a top level OR group, and inner level ANDs will be evaluated locally.