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.

PostHog feature flags documentation showing flag creation and configuration
PostHog feature flag creation interface showing boolean and multivariate options, targeting rules, and rollout percentage controls

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.

JavaScript: evaluating a boolean flag and a multivariate flag
// 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.

Python: evaluating a flag on the server
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 plan person 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 plan or company_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.

PostHog feature flag targeting rules showing person properties, cohorts, and percentage rollout options
PostHog flag targeting interface showing release conditions, person property filters, cohort targeting, and percentage rollout controls

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.

Python: enabling local evaluation with a personal API key
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.

PostHog Setup

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.

JavaScript: capturing the experiment exposure event
// 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.

TypeScript: centralising flag keys as typed constants
// 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.

JavaScript: bootstrapping flag values from server to client
// 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

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.

See the PostHog setup offer PostHog setup guide
Jake McMahon

About the Author

Jake McMahon is a product analytics strategist and founder of ProductQuant, working with B2B SaaS teams on analytics implementation, feature flag architecture, and the infrastructure that makes product decisions defensible.

The flag patterns here reflect production PostHog implementations including local evaluation at scale, kill switch design, and experiment setups that produce results teams actually act on.