Back to Blog

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

Viprasol Tech Team
June 26, 2026
13 min read

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

TypePurposeExample
Release flagControl rollout of new featuresnew-checkout-flow: 10% of users
Kill switchEmergency off switch for any featureenable-recommendations: false
Experiment flagA/B test variant assignment`pricing-page-variant: control
Permission flagGate by user attribute (plan, role)enterprise-sso: plan=enterprise
Ops flagControl system behaviormaintenance-mode: true

Vendor Comparison

LaunchDarklyUnleashFlagsmith
TypeManaged SaaSOSS + SaaSOSS + SaaS
Price$8–20/seat/monthFree (OSS), $80+/mo (SaaS)Free (OSS), $45+/mo (SaaS)
TargetingVery powerfulGoodGood
SDKsExcellentGoodGood
Edge evaluation✅ Edge SDK✅ Unleash Edge✅ Edge SDK
AnalyticsBuilt-inLimitedLimited
Best forEnterprise, complex targetingOSS preference, self-hostedSimpler 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

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol · Web Development

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.