Advanced Feature Flags: Gradual Rollouts, Kill Switches, Targeting Rules, and LaunchDarkly vs Unleash vs Flagsmith
Build a production feature flag system in 2026 — gradual percentage rollouts, kill switches, user targeting rules, operator comparisons, LaunchDarkly vs Unleash
Advanced Feature Flags: Gradual Rollouts, Kill Switches, Targeting Rules, and LaunchDarkly vs Unleash vs Flagsmith
Feature flags decouple deployment from release. You ship code to production behind a flag, then gradually roll it out — 1% of users, then 10%, then 100% — monitoring for errors at each step. If something breaks, flip the flag off in seconds rather than rolling back a deployment.
This guide covers the production patterns that make feature flags a core part of safe deployments.
Flag Types
| Type | Purpose | Example |
|---|---|---|
| Release flag | Control rollout of new features | new-checkout-flow: 10% of users |
| Kill switch | Emergency off switch for any feature | enable-recommendations: false |
| Experiment flag | A/B test variant assignment | `pricing-page-variant: control |
| Permission flag | Gate by user attribute (plan, role) | enterprise-sso: plan=enterprise |
| Ops flag | Control system behavior | maintenance-mode: true |
Vendor Comparison
| LaunchDarkly | Unleash | Flagsmith | |
|---|---|---|---|
| Type | Managed SaaS | OSS + SaaS | OSS + SaaS |
| Price | $8–20/seat/month | Free (OSS), $80+/mo (SaaS) | Free (OSS), $45+/mo (SaaS) |
| Targeting | Very powerful | Good | Good |
| SDKs | Excellent | Good | Good |
| Edge evaluation | ✅ Edge SDK | ✅ Unleash Edge | ✅ Edge SDK |
| Analytics | Built-in | Limited | Limited |
| Best for | Enterprise, complex targeting | OSS preference, self-hosted | Simpler needs, cost-conscious |
For < 50 engineers: Flagsmith OSS or Unleash OSS. For enterprise: LaunchDarkly.
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
LaunchDarkly Integration
// lib/feature-flags.ts — LaunchDarkly SDK wrapper
import LaunchDarkly from '@launchdarkly/node-server-sdk';
const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!, {
// Stream updates in real-time (vs polling every 30s)
stream: true,
// Diagnostic events opt-out
diagnosticOptOut: false,
});
// Wait for SDK to be ready before serving requests
await ldClient.waitForInitialization({ timeout: 10 });
// Define all flags with types
export const Flags = {
NEW_CHECKOUT: 'new-checkout-flow',
ENABLE_AI_SUGGESTIONS: 'enable-ai-suggestions',
MAINTENANCE_MODE: 'maintenance-mode',
MAX_UPLOAD_MB: 'max-upload-size-mb', // Numeric flag
PRICING_VARIANT: 'pricing-page-variant', // String flag for experiments
} as const;
// Type-safe flag evaluation
export async function getFlag(
flagKey: string,
user: { id: string; email: string; plan: string; country?: string },
defaultValue: boolean,
): Promise<boolean> {
const context: LaunchDarkly.LDContext = {
kind: 'user',
key: user.id,
email: user.email,
custom: {
plan: user.plan,
country: user.country ?? 'unknown',
},
};
return ldClient.variation(flagKey, context, defaultValue);
}
export async function getFlagVariation(
flagKey: string,
user: { id: string; plan: string },
defaultValue: string,
): Promise<string> {
const context: LaunchDarkly.LDContext = {
kind: 'user',
key: user.id,
custom: { plan: user.plan },
};
return ldClient.variation(flagKey, context, defaultValue);
}
Building Your Own: Redis + PostgreSQL Flag Engine
For teams that don't want vendor lock-in, a custom flag system takes ~2 days to build and covers 90% of use cases:
-- Feature flags schema
CREATE TABLE feature_flags (
key TEXT PRIMARY KEY,
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT false,
rollout_pct INTEGER NOT NULL DEFAULT 0 CHECK (rollout_pct BETWEEN 0 AND 100),
targeting_rules JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Example flags
INSERT INTO feature_flags (key, description, enabled, rollout_pct, targeting_rules) VALUES
('new-checkout', 'Redesigned checkout flow', true, 10, '[]'),
('enterprise-sso', 'SAML SSO for enterprise', true, 100, '[{"attribute":"plan","op":"eq","value":"enterprise"}]'),
('maintenance-mode', 'Kill switch for all writes', false, 0, '[]');
// lib/flags/engine.ts
import { Redis } from 'ioredis';
import { createHash } from 'crypto';
import { db } from '../db';
const redis = new Redis(process.env.REDIS_URL!);
const FLAG_CACHE_TTL = 30; // Cache flag configs for 30 seconds
interface FlagConfig {
key: string;
enabled: boolean;
rolloutPct: number;
targetingRules: TargetingRule[];
}
interface TargetingRule {
attribute: string; // 'plan', 'country', 'email'
op: 'eq' | 'neq' | 'in' | 'contains';
value: string | string[];
}
interface EvalContext {
userId: string;
plan?: string;
country?: string;
email?: string;
[key: string]: string | undefined;
}
async function getFlagConfig(key: string): Promise<FlagConfig | null> {
// Check Redis cache first
const cached = await redis.get(`flag:${key}`);
if (cached) return JSON.parse(cached);
// Load from DB
const flag = await db.featureFlags.findUnique({ where: { key } });
if (!flag) return null;
const config: FlagConfig = {
key: flag.key,
enabled: flag.enabled,
rolloutPct: flag.rolloutPct,
targetingRules: flag.targetingRules as TargetingRule[],
};
await redis.setex(`flag:${key}`, FLAG_CACHE_TTL, JSON.stringify(config));
return config;
}
function matchesTargetingRules(rules: TargetingRule[], ctx: EvalContext): boolean | null {
if (rules.length === 0) return null; // No rules → fall through to rollout
for (const rule of rules) {
const userValue = ctx[rule.attribute];
if (!userValue) return false;
switch (rule.op) {
case 'eq':
if (userValue !== rule.value) return false;
break;
case 'neq':
if (userValue === rule.value) return false;
break;
case 'in':
if (!Array.isArray(rule.value) || !rule.value.includes(userValue)) return false;
break;
case 'contains':
if (typeof rule.value === 'string' && !userValue.includes(rule.value)) return false;
break;
}
}
return true; // All rules matched
}
function isInRollout(flagKey: string, userId: string, rolloutPct: number): boolean {
if (rolloutPct === 0) return false;
if (rolloutPct === 100) return true;
// Deterministic hash: same user always gets same bucket for a given flag
const hash = createHash('md5')
.update(`${flagKey}:${userId}`)
.digest('hex');
const bucket = parseInt(hash.slice(0, 4), 16) % 100;
return bucket < rolloutPct;
}
export async function isEnabled(flagKey: string, ctx: EvalContext): Promise<boolean> {
const config = await getFlagConfig(flagKey);
// Unknown flag → safe default
if (!config) return false;
// Globally disabled → always false
if (!config.enabled) return false;
// Check targeting rules first (override rollout)
const ruleMatch = matchesTargetingRules(config.targetingRules, ctx);
if (ruleMatch !== null) return ruleMatch;
// Fallback to percentage rollout
return isInRollout(flagKey, ctx.userId, config.rolloutPct);
}
// Middleware: attach flags to request context
export async function attachFlagsMiddleware(
request: FastifyRequest,
reply: FastifyReply,
): Promise<void> {
if (!request.user) return;
const ctx: EvalContext = {
userId: request.user.id,
plan: request.user.plan,
country: request.headers['cf-ipcountry'] as string,
email: request.user.email,
};
// Evaluate all flags in parallel
const [newCheckout, aiSuggestions, maintenanceMode] = await Promise.all([
isEnabled('new-checkout', ctx),
isEnabled('enable-ai-suggestions', ctx),
isEnabled('maintenance-mode', ctx),
]);
request.flags = { newCheckout, aiSuggestions, maintenanceMode };
}
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Flag Lifecycle Management
The biggest operational problem with feature flags is accumulation. Flags that were temporary become permanent, nobody knows which ones are safe to remove, and the codebase fills with dead conditionals.
// lib/flags/lifecycle.ts
// Track flag usage to detect stale flags
export async function trackFlagEvaluation(
flagKey: string,
userId: string,
result: boolean,
): Promise<void> {
// Track: last evaluation time per flag
await redis.setex(`flag:last-eval:${flagKey}`, 90 * 24 * 3600, Date.now().toString());
// Track: unique users who evaluated this flag (HyperLogLog for memory efficiency)
await redis.pfadd(`flag:users:${flagKey}`, userId);
}
// Stale flag detection: flags not evaluated in 30+ days
export async function findStaleFlags(): Promise<string[]> {
const allFlags = await db.featureFlags.findMany({ select: { key: true } });
const stale: string[] = [];
for (const { key } of allFlags) {
const lastEval = await redis.get(`flag:last-eval:${key}`);
if (!lastEval) {
stale.push(key);
continue;
}
const lastEvalMs = parseInt(lastEval);
const daysSince = (Date.now() - lastEvalMs) / (1000 * 60 * 60 * 24);
if (daysSince > 30) stale.push(key);
}
return stale;
}
Flag governance checklist:
## Feature Flag Policy
### Creating a flag
- [ ] Add expiry date as a comment in code: `// FLAG: new-checkout, remove by 2026-08-01`
- [ ] Link to the Linear ticket tracking the rollout
- [ ] Specify rollout owner in flag description
### Monthly review
- [ ] Run staleFlags() — any flags not evaluated in 30 days?
- [ ] Check flags at 100% rollout for > 30 days → remove the flag, keep the code
- [ ] Check flags still at 0% rollout → killed feature or forgot to roll out?
### Flag removal PR checklist
- [ ] Flag removed from DB / LaunchDarkly
- [ ] All code references to flag removed
- [ ] Tests updated (no conditional logic around removed flag)
Kill Switch Pattern
Kill switches are the emergency brake. They should be:
- Evaluatable in < 5ms (cached in Redis, no DB calls)
- Off by default, on when needed (or on by default, off to disable)
- Monitored: alert if someone toggles a kill switch
// Real-time kill switch with Redis pub/sub for instant propagation
// (Don't wait for 30-second cache TTL when you need to kill something NOW)
// On flag change: invalidate cache immediately
export async function updateFlag(key: string, updates: Partial<FlagConfig>): Promise<void> {
await db.featureFlags.update({ where: { key }, data: updates });
await redis.del(`flag:${key}`); // Invalidate cache immediately
// Publish to all app instances via pub/sub
await redis.publish('flag-updates', JSON.stringify({ key, ...updates }));
}
// Subscribe to flag updates on each app instance
const flagUpdateSubscriber = new Redis(process.env.REDIS_URL!);
flagUpdateSubscriber.subscribe('flag-updates');
flagUpdateSubscriber.on('message', (_, message) => {
const update = JSON.parse(message);
// Proactively warm cache with new value
redis.setex(`flag:${update.key}`, FLAG_CACHE_TTL, JSON.stringify(update));
console.log(`Flag updated: ${update.key}`);
});
Working With Viprasol
We implement feature flag infrastructure — LaunchDarkly/Unleash integration, custom Redis-backed flag engines, gradual rollout automation, and the CI/CD practices that make flag-driven releases safe.
→ Talk to our team about deployment safety and feature flag architecture.
See Also
- A/B Testing Engineering — feature flags as the experiment substrate
- CI/CD Pipeline Setup — flag-based deployment strategy
- Distributed Systems Patterns — flags in distributed deployments
- Edge Computing — evaluating flags at the edge
- Web Development Services — safe deployment and release engineering
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.