Feature Flags in Production: Rollouts, Targeting, and Flag Hygiene
Boolean flags, percentage rollouts, user targeting, how to avoid flag debt, and why removing dead flags is as important as adding them.
Feature flags decouple deployment from release. You ship code to production but keep the feature off until you're ready. This sounds straightforward and becomes messy fast if you don't manage it deliberately.
The Basic Pattern
A feature flag is a conditional that reads from a configuration source rather than being hardcoded:
// Hardcoded — requires a deployment to change
if (false) { newFeature(); }
// Flag — can be changed without deployment
if (await flags.isEnabled('new-search-ui', userId)) { newFeature(); }The simplest implementation is a database table or environment variable. The more sophisticated you need (targeting, gradual rollouts, A/B testing), the more you need a dedicated flag service.
Flag Types
Boolean (kill switch). On or off for everyone. Used for turning off broken features quickly in production without a deployment.
const maintenanceMode = await flags.get('maintenance-mode'); // true/falsePercentage rollout. Gradually enable a feature for an increasing percentage of users. Start at 1%, watch your metrics, then ramp to 10%, 50%, 100%.
function isInRollout(userId: string, flagName: string, percentage: number): boolean {
// Hash the userId + flagName to get a consistent, stable assignment
const hash = parseInt(crypto.createHash('md5')
.update(`${flagName}:${userId}`)
.digest('hex')
.slice(0, 8), 16);
return (hash % 100) < percentage;
}Hashing the user ID ensures the same user always sees the same experience — they don't randomly flip between old and new on each page load.
User/segment targeting. Enable for specific users, organizations, or segments (beta testers, internal users, enterprise tier).
const targets = ['user123', 'user456', 'org:beta-team'];
const enabled = targets.includes(userId) || targets.includes(`org:${userOrgId}`);Multivariate / A/B. Instead of on/off, the flag returns a variant. Used for testing different UI treatments.
const variant = await flags.getVariant('checkout-button', userId);
// 'control' | 'green-button' | 'large-button'Flag Storage
For simple flags, environment variables or a database table are sufficient:
CREATE TABLE feature_flags (
name TEXT PRIMARY KEY,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
rollout_percentage INTEGER DEFAULT 0,
target_users JSONB DEFAULT '[]',
metadata JSONB,
updated_at TIMESTAMP DEFAULT NOW()
);For production use with real-time updates (no deployment needed to change a flag), you need a store with push updates or short-TTL polling. GrowthBook (used in the Claude Code / CCB project) stores flags in a database and exposes them via SDK.
Evaluation Performance
Flag evaluation happens on every request, often multiple times. It needs to be fast.
Cache flag values in memory with a short TTL (30 seconds to 5 minutes). A stale flag value for 2 minutes is almost always acceptable; a database query on every page render is not.
let cache: Map<string, { value: boolean; expiresAt: number }> = new Map();
async function isEnabled(flagName: string): Promise<boolean> {
const cached = cache.get(flagName);
if (cached && cached.expiresAt > Date.now()) return cached.value;
const flag = await db.query.featureFlags.findFirst({
where: eq(featureFlags.name, flagName)
});
const value = flag?.enabled ?? false;
cache.set(flagName, { value, expiresAt: Date.now() + 60_000 });
return value;
}Flag Debt: The Real Problem
Flags accumulate. After 12 months of shipping features behind flags and removing them from the config without removing the code, you have:
- Conditional branches throughout your codebase for features that have been 100% rolled out for months
- Tests that set up flag states that no longer matter
- Variables named
newCheckoutFlowthat is now the only checkout flow
Every flag should have a planned removal date. When a rollout hits 100% and has been stable for two weeks, the flag code should be removed — not the flag config, but the actual conditional branches in the codebase.
// This should exist for 2 weeks, then be cleaned up
if (flags.isEnabled('new-checkout')) {
return <NewCheckout />;
}
return <OldCheckout />;After rollout completes: delete the flag, delete <OldCheckout />, remove the conditional. <NewCheckout /> becomes <Checkout />.
Track flags with a linear issue or a TODO comment that links to the cleanup ticket. Make flag removal a first-class engineering task, not an afterthought.
Emergency Kill Switches
Every major feature should have a kill switch — a flag that disables it instantly without deployment. When the new search feature causes 10× CPU usage in production, you want to turn it off in 30 seconds, not deploy a rollback over 10 minutes.
This means: flag evaluation at the feature entrypoint, not deep in the implementation. A kill switch that's four layers deep in a service doesn't help you when the feature is already consuming all your resources.