How to Implement Feature Flags in Production (2026)
Feature flags let you deploy code without releasing features. Ship to production, then toggle features on for specific users. Here's how to implement them properly.
Why Feature Flags
- Safe deployments — deploy code, enable feature gradually
- A/B testing — show feature to 50% of users, measure impact
- Kill switch — disable a broken feature instantly (no redeploy)
- Beta programs — enable for specific users or organizations
- Gradual rollouts — 1% → 10% → 50% → 100%
The Options
| Tool | Best For | Pricing |
|---|---|---|
| PostHog | Feature flags + analytics | Free (1M events/mo) |
| LaunchDarkly | Enterprise-grade flags | $10/seat/mo |
| Vercel Flags | Vercel/Next.js apps | Free (with Vercel) |
| Kinde | Auth + flags combined | Free (10.5K MAU) |
| Custom | Simple needs | Free |
Option 1: PostHog (Recommended)
Feature flags + product analytics in one tool.
npm install posthog-js
Setup
// lib/posthog.ts
import posthog from 'posthog-js'
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: 'https://us.i.posthog.com',
})
export default posthog
Check Feature Flag
// Client-side
import posthog from '@/lib/posthog'
function Dashboard() {
const showNewUI = posthog.isFeatureEnabled('new-dashboard-ui')
if (showNewUI) {
return <NewDashboard />
}
return <OldDashboard />
}
Server-Side (Next.js)
import { PostHog } from 'posthog-node'
const posthog = new PostHog(process.env.POSTHOG_API_KEY!)
export default async function DashboardPage() {
const flags = await posthog.getAllFlags('user-123')
if (flags['new-dashboard-ui']) {
return <NewDashboard />
}
return <OldDashboard />
}
Create Flag in PostHog Dashboard
- Go to Feature Flags → New Flag
- Set key:
new-dashboard-ui - Set rollout: 10% of users (or specific users/groups)
- Save and deploy
Option 2: Custom Implementation (Simple)
For simple boolean flags without a third-party service:
// lib/flags.ts
type FeatureFlags = {
newDashboard: boolean
aiAssistant: boolean
darkMode: boolean
}
const flags: Record<string, FeatureFlags> = {
production: {
newDashboard: false,
aiAssistant: true,
darkMode: true,
},
staging: {
newDashboard: true,
aiAssistant: true,
darkMode: true,
},
}
export function getFlags(): FeatureFlags {
const env = process.env.NODE_ENV === 'production' ? 'production' : 'staging'
return flags[env]
}
export function isEnabled(flag: keyof FeatureFlags): boolean {
return getFlags()[flag]
}
Usage
import { isEnabled } from '@/lib/flags'
if (isEnabled('newDashboard')) {
return <NewDashboard />
}
Database-Backed Flags (More Flexible)
// Store flags in your database
const flags = pgTable('feature_flags', {
key: text('key').primaryKey(),
enabled: boolean('enabled').default(false),
rolloutPercentage: integer('rollout_percentage').default(0),
allowedUserIds: text('allowed_user_ids').array(),
})
export async function isEnabled(flagKey: string, userId: string): Promise<boolean> {
const flag = await db.query.flags.findFirst({
where: eq(flags.key, flagKey),
})
if (!flag || !flag.enabled) return false
// Check if user is in allowlist
if (flag.allowedUserIds?.includes(userId)) return true
// Check rollout percentage
const hash = simpleHash(userId + flagKey)
return (hash % 100) < flag.rolloutPercentage
}
Option 3: Vercel Flags (Next.js)
// flags.ts
import { flag } from '@vercel/flags/next'
export const newDashboard = flag({
key: 'new-dashboard',
decide: () => false, // Default value
})
// In a Server Component
import { newDashboard } from '@/flags'
export default async function Page() {
const showNew = await newDashboard()
return showNew ? <NewDashboard /> : <OldDashboard />
}
Best Practices
1. Name Flags Clearly
✅ new-checkout-flow
✅ ai-assistant-v2
✅ premium-analytics
❌ flag1
❌ test
❌ johns-feature
2. Clean Up Old Flags
Remove flags after full rollout. Stale flags create technical debt.
Flag created: Jan 1
Rolled out to 100%: Jan 15
Flag removed from code: Jan 22 ← Don't skip this!
3. Use Server-Side Evaluation
Evaluate flags on the server when possible. Client-side evaluation can flicker (show old UI, then switch to new).
4. Have a Kill Switch
Every major feature should have a flag. If something breaks in production, disable the flag instantly — no rollback needed.
5. Log Flag Evaluations
Track which users see which variants. Essential for debugging and A/B test analysis.
Gradual Rollout Strategy
Day 1: Enable for internal team (QA)
Day 3: Enable for 5% of users
Day 5: Check metrics — errors, performance, user feedback
Day 7: Enable for 25% of users
Day 10: Enable for 50% of users
Day 14: Enable for 100% of users
Day 21: Remove flag from code
FAQ
Do feature flags slow down my app?
With server-side evaluation: negligible. With client-side: depends on SDK initialization time (~50-100ms for PostHog). Cache flag values to minimize impact.
How many flags should I have?
As few as possible. 10-20 active flags is manageable. 100+ becomes technical debt. Clean up after rollout.
Should every feature use a flag?
Major features and risky changes: yes. Small bug fixes and copy changes: no. Use judgment.
PostHog vs LaunchDarkly?
PostHog: free tier, includes analytics, good for startups. LaunchDarkly: more flag features, better for large enterprises. Most teams: PostHog.
Bottom Line
Feature flags are essential for safe production deployments. Start with PostHog (free, includes analytics) or build a simple custom solution. The key patterns: gradual rollout, kill switches, and cleaning up old flags. Every major feature should ship behind a flag.