Loading tutorials…
Loading tutorials…
Feature flags are the cheap insurance product teams skip until their first bad deploy. PostHog makes them free — but the targeting rules, SSR caveats, and cleanup discipline are not obvious. This walks through all of it.
Who this is forEngineering teams shipping features without a flag system, or PostHog users who have used flags for a/b tests but want full rollout control. Particularly important if you ship to production daily and need fast rollback.
What you'll need
Step 1
Boolean flags (on/off) are 80% of use cases. Multi-variant flags (control / variant_a / variant_b) are for experiments. Pick before creating.
Boolean: `new_checkout_flow` is on or off. Use for kill-switches, gradual rollouts, beta-user gating.
Multi-variant: `pricing_page_test` has variants `control`, `version_a`, `version_b`. Use when you want users in different cohorts to see different experiences AND you want to track outcomes per variant (this is what experiments use under the hood — see tutorial #6).
Naming convention: `verb_noun_descriptor` in snake_case. Examples: `enable_new_checkout`, `show_dark_mode`, `gate_enterprise_features`. Avoid temporal names (`v2_dashboard` will be confusing once v3 ships).
In PostHog → Feature Flags → New feature flag. Pick the flag type. Add a clear description: what does this flag do, who owns it, what is the planned removal date.
Step 2
Define WHO gets the flag: % of all users, a specific cohort, a property match, or an internal-team email list. Layer conditions for staged rollouts.
Open the flag → Release Conditions.
Simple percentage rollout: "Release to 10% of users" — PostHog hashes the distinct_id and consistently shows the flag to the same 10% (stable across sessions).
Cohort match: "Release to users where `plan = pro`" or "Release to users in cohort: Active Last 7 Days." PostHog cohorts are precomputed and refresh hourly.
Specific user list: "Release to users where `email IN [team@acme.com, beta1@..., beta2@...]`". Use for internal-team and selected beta-tester rollouts.
Layered: "Release to 100% of users where `plan=pro` AND 10% of users where `plan=free`." Use for staged rollouts that prioritize paid users.
Set a "Default" rule (what happens if no condition matches) — usually FALSE for new features, TRUE for kill-switches you are sunsetting.
Step 3
Use `posthog.isFeatureEnabled("flag_name")` (boolean) or `posthog.getFeatureFlag("flag_name")` (multi-variant). Wait for flags before rendering gated UI.
Client-side React: `import { useFeatureFlagEnabled } from "posthog-js/react"; const showNewCheckout = useFeatureFlagEnabled("new_checkout_flow"); return showNewCheckout ? <NewCheckout/> : <LegacyCheckout/>;`
Multi-variant: `import { useFeatureFlagVariantKey } from "posthog-js/react"; const variant = useFeatureFlagVariantKey("pricing_test"); switch(variant) { case "version_a": return <PricingA/>; ... }`
Wait for flags to load before deciding (otherwise flags may be undefined on first render): `import { useFeatureFlagPayload, usePostHog } from "posthog-js/react"; const posthog = usePostHog(); const [ready, setReady] = useState(false); useEffect(() => posthog.onFeatureFlags(() => setReady(true)), []);`. Render a skeleton until ready.
Server-side Node: `const enabled = await phServer.isFeatureEnabled("flag_name", distinctId, { personProperties: { plan: "pro" } });` — pass person properties manually because server has no session context.
Default to FALSE if the flag check errors. Never let a flag-system outage block a critical UX.
Step 4
Server Components cannot call `posthog-js`. Use `posthog-node` server-side with explicit distinct_id, or defer flag-gated UI to Client Components.
In a Server Component, import `PostHog` from `posthog-node`: `import { PostHog } from "posthog-node"; const ph = new PostHog(process.env.POSTHOG_KEY!, { host: "..." }); const enabled = await ph.isFeatureEnabled("flag", userId, { personProperties: {...} }); ph.shutdown();`.
Pass user properties explicitly — the server SDK has no session context. If the flag targets `plan=pro`, you must pass `personProperties: { plan: user.plan }`.
Call `await ph.shutdown()` after every request in serverless environments. Otherwise events queue in memory and disappear when the function dies.
For Client Components, the hooks work normally: `useFeatureFlagEnabled`.
Watch for hydration mismatches: if the server renders the flag-on version and the client initially renders flag-off (before PostHog loads), React will throw a hydration warning. Use `suppressHydrationWarning` or a delayed-render pattern for flag-gated content above the fold.
Step 5
Start at 1% → check error rate + key metric → 10% → check → 50% → 100%. Track a key conversion event broken out by flag exposure.
Initial release: 1% of users. Monitor your error tracking (Sentry / PostHog Error Tracking) for new errors. If error rate spikes, kill the flag immediately.
After 24 hours at 1% with stable errors: bump to 10%. Open a Trends insight broken out by flag value (`feature_flag_response.flag_name`).
After 48 hours at 10% with healthy metrics: bump to 50%. Now A/B-test the metric — is the cohort with flag=true converting at the same or better rate as flag=false?
After 7 days at 50% with positive results: bump to 100% and start the flag-removal countdown.
If at any stage error rate spikes, conversion drops, or you get a wave of support tickets — flip the flag back to 0%. Rollback is one click. Use it.
Step 6
Stale flags are tech debt. Once a flag is at 100% for 14 days, plan removal. Delete the flag from PostHog and remove the code check.
Open Feature Flags monthly. Sort by "Last evaluated". Anything not evaluated in 30+ days is dead — investigate.
For flags at 100% rollout for 14+ days: open a PR to remove the flag check from code. Then delete the flag from PostHog.
For flags at 0% rollout for 14+ days: same — remove the code and delete the flag. (The feature was killed; finish the cleanup.)
Use the `cleaning-up-stale-feature-flags` workflow if you have many: PostHog Feature Flags page → Filter by status → sort by Last Evaluated → archive in batches.
Target: <30 active flags at any time. Above that, the codebase becomes a flag-checking maze.
Common mistakes
Forgetting the default value
What goes wrong: Flag fails to load (network blip, PostHog incident). Your code does `if (posthog.isFeatureEnabled("flag"))` → returns undefined → falsy → user gets the old experience for a feature you already migrated away from. Conversion drops 8% for ~30 minutes during the incident.
How to avoid: Always set a sensible default in the second argument: `posthog.isFeatureEnabled("new_checkout", false)` for new features, `true` for kill-switches. Treat the default as the fallback when the network is down.
Server-side flag checks without person properties
What goes wrong: Flag is targeted "users where plan=pro". Server-side check passes only the distinct_id without the `plan` property. The server SDK does not know the user is pro (no session). The flag returns false. Pro users see the wrong experience.
How to avoid: Always pass `personProperties` AND `groupProperties` (if using groups) to server-side flag calls. Read the user record from your DB inside the same request and pass everything the flag might target on.
Stale flags that no one removes
What goes wrong: After two years, the codebase has 140 flags. Most are at 100% but the code check is still there. Refactors are blocked because no one knows which flags are safe to remove. Onboarding new engineers takes weeks longer because they cannot read the conditional logic. Cost: 30-50 hours of engineering per year on flag-related friction.
How to avoid: Assign an owner to every flag at creation. Add a "remove by" date. Block PRs that add new flags without a removal plan. Quarterly: cleanup sprint to remove stale flags. Cap active flags at ~30.
Using flags for permanent config
What goes wrong: You use a flag to gate `enterprise_only_feature` (permanent business logic, not a temporary rollout). Now every page load makes a flag check for something that will never change. Latency creeps up by 30-50ms. Worse, the business logic is now coupled to PostHog's uptime.
How to avoid: Use flags ONLY for temporary rollouts (weeks-to-months). For permanent business logic, use your database / config / user-role system. Flag system uptime should never be a hard dependency for paid features.
Hydration mismatch on Next.js
What goes wrong: Server renders flag-off version. Client loads PostHog and re-renders flag-on. React throws hydration warning, content flashes, Lighthouse score drops. CLS metric tanks. SEO hit on Core Web Vitals.
How to avoid: For above-the-fold flag-gated content on SSR: check flag server-side using `posthog-node` and pass it as a prop. Or use a delayed-render skeleton that does not change layout between server / client.
No monitoring during rollout
What goes wrong: You bump a flag from 10% to 100%. The new code path has a memory leak. Within 2 hours, average session length drops 40%, users complain on Twitter, you lose ~$3,000-8,000 of conversions before noticing.
How to avoid: During rollouts, open a dashboard with: error rate, key conversion event, average session length, broken out by flag exposure. Monitor for 24 hours minimum at each step. Roll back at the first sign of regression.
Recap
Done — what's next
How to set up PostHog event tracking with a taxonomy that scales
Read the next tutorial
Hand it off
Feature flags look simple in the docs. The complexity is in flag hygiene, targeting strategy, rollout discipline, and integration with your error monitoring. Most engineering teams under-invest in this and pay for it in bad deploys 12 months later. EverestX matches you with a vetted PostHog specialist who has built flag systems for 20+ teams, from $14-16/hr.
See specialist rates
Yes — 1M flag requests per month is the free tier and covers most products under 50K MAU. Past that you upgrade to a paid plan; flag-request pricing is ~$0.0001 per request after free tier (essentially negligible).
A flag is the targeting + rollout mechanism. An experiment is a flag + a statistical framework for measuring outcomes. Every PostHog experiment uses a flag under the hood. If you don't need significance testing, a plain flag is enough.
Yes — PostHog Groups let you target "Release to all users where account.plan=enterprise". You need to call `posthog.group("account", accountId, { plan: "enterprise" })` on init for it to work. Useful for B2B SaaS where rollouts happen per-customer not per-user.
Your code falls back to the default value you provided in `isFeatureEnabled('flag', defaultValue)`. PostHog also has a local-evaluation mode where flags are evaluated against locally-cached rules — useful for high-traffic apps that cannot tolerate any latency. Configure with `posthog-node` + `featureFlagsPollingInterval`.
Yes — kill-switches on revenue-critical paths are a best practice. If checkout breaks, you can flip a flag to route all users to a stable backup flow instantly. Just always set the default to TRUE (working state) so a flag-system outage does not break checkout.
PostHog
Most teams ship 50 events in week one, then spend month four rewriting them because the names made no sense in retrospect. This walks through an event taxonomy that scales, a property schema that does not drift, and the identify flow that keeps your funnel reports honest.
PostHog
Running tests is easy. Running tests that produce real decisions is hard. This walks through hypothesis design, sample-size calculation, the PostHog experiment UI, and the 5 statistical mistakes that invalidate 80% of DIY A/B tests.
PostHog
Session replay is the most valuable PostHog feature for debugging product UX — and the most dangerous if you skip the masking step. This walks through enabling replay, configuring DOM-level privacy, controlling storage cost, and the compliance checklist.
PostHog
DIY PostHog is the right call up to a point. Then it isn't. This is the honest framework: when the cost of self-managing exceeds the cost of hiring, and how to tell which side you're on.