TL;DR
- PostHog feature flags come in two types: boolean (on/off) and multivariate (returns one of several string values). Experiments are multivariate flags with a goal metric attached.
- Targeting rules match on person properties, cohorts, group properties (if group analytics is set up), or a percentage rollout. Rules combine with AND logic; multiple release conditions combine with OR.
- Local evaluation caches the flag rule set on your server and evaluates in memory, reducing latency from 20-100ms per request to under 1ms. Available in server SDKs only.
- Feature flags are free on PostHog's free plan up to 1 million requests per month. Local evaluation requests do not count toward this quota.
- Every PostHog experiment is backed by a multivariate feature flag. You can use the flag independently for rollouts and then attach an experiment later.
- Flag debt is real. Any flag at 100% rollout for more than 30 days should be removed from the codebase, not just disabled in PostHog.
Boolean flags vs multivariate flags: which to use
Boolean flags answer a single yes-or-no question. Should this user see this feature? They are the right choice for gradual rollouts, kill switches, and access control. A new dashboard feature rolling out to 10% of users. A beta toggle for early adopters. A kill switch to disable a feature without deploying new code. All of these are boolean flags.
Multivariate flags return a string value from a defined set of variants. They answer a different question. Which version of this experience should this user see? Use them when the variants are meaningfully different from each other and you want to track each independently. A pricing page test with three price points. An onboarding flow with two different step sequences. A CTA button with four copy variants.
The distinction matters for how you write the code that consumes the flag. A boolean flag check is a conditional. A multivariate flag check is a switch statement or a lookup. Mixing them up produces subtle bugs where a flag returns null (flag not loaded yet) and the code treats null as the falsy case of a boolean check, showing the wrong default state.
A useful rule of thumb settles most cases. If the flag answers more than two meaningful options, use multivariate. If it answers exactly two, boolean is simpler. If it has exactly two, boolean is simpler and less error-prone.
Creating and evaluating your first flag
Create flags in the PostHog UI under the Feature Flags section, give it a key in kebab-case, and it is immediately evaluable from any SDK. The flag key is the identifier you use in code. Choose it deliberately. It will appear in dozens of places across your codebase before you remove it.
Client-side flag evaluation (JavaScript)
The PostHog JavaScript SDK evaluates flags after posthog.init() completes and the flag values have been fetched from the PostHog API. Flag values are not available synchronously on the first render. Either gate the flag-dependent UI to render only after flags are loaded, or use the onFeatureFlags callback.
// Boolean flag — returns true/false/undefined
if (posthog.isFeatureEnabled('new-dashboard')) {
renderNewDashboard();
} else {
renderLegacyDashboard();
}
// Multivariate flag — returns a string variant or undefined
const pricingVariant = posthog.getFeatureFlag('pricing-page-test');
// pricingVariant: 'control' | 'price-lower' | 'price-higher' | undefined
switch (pricingVariant) {
case 'price-lower': return ;
case 'price-higher': return ;
default: return ;
}
// Run code once flags are ready (avoids rendering before flags load)
posthog.onFeatureFlags(() => {
if (posthog.isFeatureEnabled('new-dashboard')) {
initNewDashboard();
}
});
The undefined return when a flag has not loaded yet is the most common source of flag-related bugs in production. The default case in a switch statement and the else branch in a boolean check should both render the safe, established experience, not the new one. Treat undefined as flag-off.
Server-side flag evaluation
Server-side flag evaluation is the right approach whenever the flag decision needs to happen before the client renders, when you are gating API endpoints, or when flag values need to be consistent across a request that spans multiple services.
from posthog import Posthog
client = Posthog('phc_your_key', host='https://us.i.posthog.com')
# Evaluate for a specific user
is_enabled = client.feature_enabled('new-dashboard', user_id)
# Multivariate
variant = client.get_feature_flag('pricing-page-test', user_id)
# With person properties (used when user has no PostHog profile yet)
variant = client.get_feature_flag(
'pricing-page-test',
user_id,
person_properties={'plan': 'growth', 'company_size': 50}
)
Server-side evaluation without local evaluation makes one network request to PostHog per get_feature_flag() call. On a high-traffic endpoint this adds 20-100ms of latency per request and costs flag request quota. Local evaluation solves both problems.
Targeting rules: who sees what and when
PostHog flag targeting rules match users against conditions defined on person properties, cohorts, group properties, or flag-specific properties you pass at evaluation time. Within a single release condition, rules combine with AND logic. Multiple release conditions on the same flag combine with OR. A user who matches any release condition gets the flag enabled.
The most common targeting patterns in production:
- Internal team only: add a release condition matching
email contains @yourcompany.com. Ship features to your team in production before any user sees them. - Beta cohort: create a cohort of users who signed up for early access, then target the cohort directly. The flag evaluates against PostHog's cohort membership, not a property you have to keep in sync.
- Plan tier: match on the
planperson property. Roll a feature to Enterprise plan users first, Growth next, Starter last. - Company-level rollout: with group analytics active, match on group properties like
planorcompany_size. Every user in matching companies gets the flag, regardless of their individual properties. - Percentage rollout: set a rollout percentage within a release condition. PostHog uses a deterministic hash of the user ID and flag key to assign users consistently. The same user always gets the same variant.
Combine targeting with percentage rollouts by setting both a condition and a percentage in the same release condition block. This targets 20% of Enterprise plan users specifically, rather than 20% of all users who happen to be on the Enterprise plan.
Local evaluation: fast flags for server-side use
Local evaluation downloads the complete flag rule set to your server once and evaluates every subsequent flag check in memory, without a network call. Flag evaluation drops from 20-100ms per request to under 1ms. This makes feature flags viable on hot paths, middleware, and API endpoints where a network round-trip would be unacceptable.
Local evaluation is available in the server-side SDKs. Node, Python, Ruby, Go, PHP, and Java all support it. It is not available in the browser JavaScript SDK or the mobile SDKs, where the SDK fetches flag values from the API once per session and caches them client-side.
from posthog import Posthog
# Local evaluation requires a personal API key (not the project API key)
# Generate one at posthog.com/settings/user-api-keys
client = Posthog(
project_api_key='phc_your_project_key',
host='https://us.i.posthog.com',
personal_api_key='phx_your_personal_api_key', # enables local eval
)
# All flag evaluations now happen locally after the initial rule set fetch
# No network call per evaluation — results in < 1ms evaluation time
is_enabled = client.feature_enabled('new-dashboard', user_id)
The SDK refreshes the local rule set every 30 seconds by default. Flag changes you make in the PostHog UI propagate to local evaluation within that window. For kill-switch use cases where you need immediate propagation, lower the polling interval or disable local evaluation for that specific flag and fall back to remote evaluation.
Local evaluation requests do not consume your feature flag request quota. This matters at scale. A service handling 10,000 requests per second would consume 864 million flag requests per day without local evaluation.
That number pays for local evaluation setup many times over.
Feature flags wired up correctly from the start.
PostHog feature flags need the right flag key conventions, targeting rules, local evaluation config, and experiment design to produce trustworthy results. ProductQuant sets this up as part of the two-week PostHog sprint.
Running A/B experiments with feature flags
Every PostHog experiment is a multivariate feature flag with a goal metric and a statistical significance engine attached. Create an experiment in the Experiments section, PostHog creates the underlying flag automatically, and from that point the flag key is available in all SDKs exactly like any other flag.
Choosing a goal metric
The goal metric is the event that determines whether a variant wins. It must be an event that is already firing reliably in PostHog before the experiment starts. Running an experiment on a goal metric that has tracking gaps produces results you cannot trust, even if the statistical significance looks clean.
Choose a metric that is sensitive enough to move within your experiment window but not so noisy that random variation produces false significance. Conversion events (checkout completed, subscription started, feature activated) work well. Page view counts and generic click events work poorly because they are high-volume and underdetermined.
Minimum sample size and significance
PostHog calculates minimum sample size requirements before the experiment runs based on your expected baseline conversion rate and the minimum detectable effect you specify. Run the experiment until the recommended sample size is reached in each variant before reading the results. Stopping early when one variant looks better is the most common way to get a false positive.
PostHog uses a Bayesian statistical model, reporting the probability that each variant beats the control rather than a p-value. A result showing "variant B has 95% probability of beating control" means the experiment has enough data to draw a conclusion. Below 90%, keep running.
// When a user sees the variant, capture the experiment viewed event
// PostHog uses this to track experiment exposure correctly
const variant = posthog.getFeatureFlag('checkout-cta-experiment');
if (variant) {
// PostHog automatically captures $feature_flag_called on isFeatureEnabled()
// For experiments, also fire the specific goal event when the target action occurs
posthog.capture('checkout_completed', {
'$feature/checkout-cta-experiment': variant, // ties the conversion to the variant
});
}
Including the $feature/flag-key property on goal events lets PostHog's experiment engine correctly attribute conversions to the variant the user saw, even if the conversion happens in a different session from the initial exposure.
Early access features and opt-in flags
PostHog's Early Access Management feature is a layer on top of feature flags that lets users opt themselves into unreleased features. You define the early access feature in PostHog, connect it to a flag, and PostHog renders a widget in your product that lets eligible users toggle access on or off.
Early access features are useful when you want to give motivated users access to a beta without manually managing a list. Users who opt in get the flag enabled on their profile. Their usage data flows into PostHog analytics the same way any flag-gated user's data does, so you get real usage metrics from the users most likely to engage with the feature deeply.
The widget is embeddable and stylable. Add it to a settings page, a changelog announcement, or an in-app notification. Users who opt in are automatically added to a PostHog cohort that you can use for targeting other flags or sending surveys.
Flag debt: the cost of flags that never get removed
Flag debt is the technical debt that builds when feature flags are shipped but the code paths they gate are never cleaned up. A codebase with 50 active flags that has been accumulating them for two years likely has 30 flags that are either fully rolled out, permanently disabled, or controlling features that no longer exist.
The cost is not the flags in PostHog. It is the if (isFeatureEnabled('legacy-flag-from-2024')) branches in the codebase that every engineer has to mentally parse when reading that code. They slow comprehension, create confusion about which code path is actually active, and occasionally cause bugs when someone assumes the flag is off and removes the wrong branch.
Three practices that contain flag debt
Adding a removal date when creating a flag is the most effective single practice. Add a line to the flag description that says when to remove it, for example a note to remove after 2026-09-01 if the rollout is complete. It takes 30 seconds and turns the flag from an indefinite commitment into a time-boxed one.
PostHog's flag usage insights show you which flags have been at 100% rollout for an extended period. Review this list monthly. Any flag that has been at 100% for more than 30 days is a removal candidate. The removal process has two steps. Delete the flag code path from the codebase, keeping the non-flagged version live. Then archive the flag in PostHog.
Do not just disable flags in PostHog and leave the code in place. The code path still exists, still needs to be read, still creates cognitive overhead. The flag removal is only complete when the branch is deleted from the codebase.
Implementation patterns worth knowing
Three flag implementation patterns come up in almost every production PostHog setup and are worth standardising before you have dozens of flags in flight.
The flag constant pattern
Define all flag keys as constants in a single file rather than scattering string literals across the codebase. This makes it easy to find every call site for a flag when it is time to remove it, and it prevents typos in flag key strings from silently returning undefined.
// flags.ts — single source of truth for all flag keys
export const FLAGS = {
NEW_DASHBOARD: 'new-dashboard',
PRICING_TEST: 'pricing-page-test',
EARLY_ACCESS_AI: 'early-access-ai-features',
} as const;
// Usage — no string literals in component code
if (posthog.isFeatureEnabled(FLAGS.NEW_DASHBOARD)) {
// ...
}
The bootstrap pattern for server-rendered apps
In server-rendered applications, evaluate all flags server-side at request time and pass the results to the client as bootstrap data. The client initialises PostHog with the pre-evaluated flag values, making them available synchronously on the first render without a separate network fetch.
// Server: evaluate flags for the authenticated user
const flags = await posthogServer.getAllFlags(req.user.id);
// Pass to page as JSON (Next.js example)
return {
props: {
bootstrapData: {
distinctId: req.user.id,
featureFlags: flags,
}
}
};
// Client: initialize PostHog with bootstrap values
posthog.init('phc_...', {
bootstrap: {
distinctId: pageProps.bootstrapData.distinctId,
featureFlags: pageProps.bootstrapData.featureFlags,
}
});
The kill switch pattern
Keep at least one boolean flag per major feature or integration as a production kill switch. When a feature causes an incident, flipping a flag to off and waiting 30 seconds is faster than a hotfix deploy. Kill switches should default to on (the feature is active), so that a new flag being created does not accidentally disable the feature before targeting rules are configured.
FAQ
Are PostHog feature flags free?
Yes, up to 1 million flag requests per month on the free plan. Requests beyond that are billed per million. Local evaluation does not count against this quota, making it essentially free at scale on server-side implementations.
What is the difference between a feature flag and an experiment in PostHog?
A flag controls access. An experiment measures impact. Every experiment is backed by a multivariate flag, but a flag can exist independently for rollouts without an experiment attached. The experiment layer adds a goal metric, minimum sample size calculation, and Bayesian significance engine on top of the flag.
What is local evaluation in PostHog feature flags?
Local evaluation caches the flag rule set on your server and evaluates flags in memory, eliminating the network call per evaluation. Latency drops from 20-100ms to under 1ms. Available in server SDKs only. Requires a personal API key. Local evaluation requests do not count toward your monthly flag quota.
Can PostHog feature flags target by company or account?
Yes, if group analytics is set up. With groups active, flag targeting rules can include group properties like plan tier, company size, or industry. Without group analytics, targeting is limited to person properties and percentage rollouts.
How do I avoid stale flags and flag debt in PostHog?
Set a removal date in the flag description when you create it. Use PostHog's flag usage insights to identify flags at 100% rollout for over 30 days. Delete the flag code path from the codebase when removing. Disabling the flag in PostHog without removing the code branch leaves the debt in place.
Sources
- PostHog Feature Flags Documentation: flag creation, targeting rules, rollout configuration, and SDK integration
- PostHog Local Evaluation: server-side local evaluation setup, polling interval config, and quota implications
- PostHog Experiments Documentation: experiment setup, goal metric selection, sample size calculation, and significance reporting
- PostHog Early Access Management: opt-in feature flags and the early access widget
- ProductQuant feature flag implementation experience: flag key conventions, bootstrap patterns, kill switch design, and flag debt management across multiple B2B SaaS products
PostHog Feature Flags
Flags that work. Rollouts you can trust.
Flag key conventions, targeting rules, local evaluation, experiment design, and the discipline to remove flags when they are done. ProductQuant sets this up in the two-week PostHog sprint.



