guide·intermediate·updated 2026-09-04

Feature Flags Done Right: Gradual Rollouts Without Breaking Prod

Learn feature flags, gradual rollouts, and canary deployments. Step-by-step implementation in Node.js without expensive SaaS tools. Real production patterns.

feature flagsgradual rolloutscanary deploymentsNode.js

TL;DR: Stop deploying to 100% of users and hoping. Feature flags let you roll out to 1% → 5% → 25% → 100%, catch bugs early, and kill a broken feature in 30 seconds (not 30 minutes). Production-ready implementation included.


The Problem: Deploy → Panic

Your app ships. Something breaks. Now you have two choices:

Option A (Without flags): Revert the commit, wait for CI (5–10 min), redeploy (another 5–10 min), wait for pods to spin up. Total recovery: 15–30 minutes. During that time, 10K users see the broken behavior.

Option B (With flags): Click a toggle. Feature disabled in 30 seconds. Zero downtime, zero redeploy, zero emergency calls.

This is why every founder who's had a production incident asks: "How do we safely ship without breaking everything?"


What Are Feature Flags?

A feature flag is a conditional that wraps code at runtime:

if (isFeatureEnabled('new_checkout')) {
  // New payment logic
} else {
  // Old payment logic (safe fallback)
}

The key insight: Deployment ≠ Release

  • Deployment: Code goes to production (happens once, instantly)
  • Release: Users see the feature (you control this separately) This separation is what makes safe rollouts possible.

Why This Matters: Real Production Incidents

Most teams discover the need for feature flags the hard way—after a production incident where code deployed but broke for all users at once.

Real scenario (Scenario 1): Payment processor API response format changed. Team deployed without testing both old and new format. Payment page down for 20 minutes. Revenue loss: ~$8K/min.

With flags: Feature deployed behind a flag. Rolled out to 1% first. Payment API degradation caught. Rolled back in 10 seconds. Revenue loss: ~$170.

The difference: rollout strategy, not luck.


The Rollout Pattern That Works

This is the pattern every major tech company uses (verified across LaunchDarkly, PostHog, Unleash, and Datadog docs):

Internal Team (0.1%)

Beta Users (1%)

Early Adopters (5%)

Small Sample (25%)

Majority (50%)

Everyone (100%)

Why these percentages?

Percentage-based traffic deployment routes 1%, 5%, 25%, and then 100% of live traffic to the new version, monitoring metrics at each step.

Each step roughly doubles exposure:

  • At 1%, a critical bug affects 1 user per 100
  • At 5%, it affects 5 users per 100
  • At 100%, it affects everyone You get multiple "checkpoints" to catch issues before they become crises.

Metrics to watch at each stage:

Success metrics like error rates under 0.1% or latency staying neutral signal when to move forward or pause the release.


Implementation: Build Your Own (No SaaS Needed)

Option 1: Redis-Backed Flags (Recommended)

Redis is fast, scales easily, and evaluating flags is simple:

const redis = require('redis');
const client = redis.createClient();
 
// Initialize flags
async function initFlags() {
  await client.hSet('feature_flags', {
    'new_checkout': '{"enabled": false, "rollout_percentage": 0}',
    'dark_mode': '{"enabled": true, "rollout_percentage": 100}'
  });
}
 
// Evaluate flag for user
async function isFeatureEnabled(featureName, userId) {
  const flagData = await client.hGet('feature_flags', featureName);
  
  if (!flagData) return false;
  
  const { enabled, rollout_percentage } = JSON.parse(flagData);
  if (!enabled) return false;
  
  // Hash user ID to percentage (0-100)
  const hashValue = hashUserId(userId);
  return hashValue < rollout_percentage;
}
 
// Hash function for consistent user assignment
function hashUserId(userId) {
  const hash = require('crypto')
    .createHash('md5')
    .update(userId)
    .digest('hex');
  return parseInt(hash.substring(0, 2), 16) % 101; // 0-100
}
 
// In your Express route
app.get('/checkout', async (req, res) => {
  const useNewCheckout = await isFeatureEnabled('new_checkout', req.user.id);
  
  if (useNewCheckout) {
    return res.json(await newCheckoutFlow(req.user));
  } else {
    return res.json(await legacyCheckoutFlow(req.user));
  }
});

Key insight: The hash function ensures the same user always sees the same variant (consistent experience), while distributing users evenly across the percentage range.

Option 2: Database-Backed Flags (If No Redis)

const db = require('./db');
 
async function isFeatureEnabled(featureName, userId) {
  const flag = await db.query(
    `SELECT enabled, rollout_percentage FROM feature_flags WHERE name = ?`,
    [featureName]
  );
  
  if (!flag || !flag.enabled) return false;
  
  const hashValue = hashUserId(userId);
  return hashValue < flag.rollout_percentage;
}
 
// Update flag percentage from admin dashboard
async function setRolloutPercentage(featureName, percentage) {
  await db.query(
    `UPDATE feature_flags SET rollout_percentage = ? WHERE name = ?`,
    [percentage, featureName]
  );
}

Production Rollout Checklist

Before You Start:

  • Feature is behind a flag in code
  • Flag defaults to false (feature hidden)
  • Both code paths tested (with flag on + off)
  • Metrics defined (error rate, latency, business metrics)

Rollout Day:

  • Enable for internal team only (0.1%)
  • Monitor error logs and dashboards for 10 minutes
  • If healthy, expand to 1% of production traffic
  • Monitor error rate target: < 0.1% (verified against Datadog and multiple sources)
  • If metrics stay green for 30 minutes, expand to 5%
  • Continue doubling: 5% → 25% → 50% → 100%
  • Each stage takes 30–60 minutes

If Something Goes Wrong:

Just disable the flag. No redeploy needed. Instead of reverting a commit, waiting for CI, and redeploying, you toggle a flag and the problematic feature is disabled in seconds.

After 100% Rollout:

Once a flag has been at 100% for a defined period (one to two weeks is common), remove it.

Why? Stale flags accumulate. A codebase with 500 old flags becomes unmaintainable.


Real Production Scenario: Payment Feature Rollout

Setup: New payment processor (Stripe API v2) with different response format. Old code expects charge.id, new API returns charge_id.

Day 1:

0% → Deploy code behind 'new_payment_v2' flag
    Flag is OFF, payment goes through legacy path
    
10:00 AM → Enable for 0.1% (internal team)
    You test 50 payments
    All succeed, error rate: 0%
    
10:15 AM → Expand to 1% (100 real users)
    Monitor error logs
    Error rate: 0%
    Latency: normal
    
10:45 AM → Expand to 5% (500 real users)
    Monitor conversion rate
    Conversions: +0.2% (good)
    Error rate: 0%
    
11:30 AM → Expand to 25% (2,500 users)
    Still green
    
2:00 PM → Expand to 50% (5,000 users)
    Overnight test: no issues reported in logs
    
Next morning → Expand to 100%
    All users on new API
    
1 week later → Remove flag from code
    Old payment code path deleted
    Feature flagging complete

If something went wrong at 5%:

You'd catch it before 500 users are harmed. Toggle the flag OFF. Revenue protected. No emergency hotfix.


When to Use Paid Tools vs DIY

DIY (What We Just Built):

Best for:

  • Solo founder or small team (< 20 engineers)
  • Targeting by percentage only (no complex user segments)
  • Single environment (prod only)
  • Budget-conscious ❌ Gaps:
  • No targeting by country/subscription tier
  • No audit logs (who disabled this flag?)
  • No scheduled rollouts (automate expansion)
  • Manual dashboard to flip flags

Paid Tools (Unleash, PostHog, LaunchDarkly):

Unleash is one of the most popular open-source feature flag platforms for teams that want control and flexibility. PostHog combines feature flags with product analytics, session replay, and A/B testing in one platform.

Managed tools provide:

  • Automatic rollout scheduling (expand to 5% in 1 hour, auto)

  • Advanced targeting (country, subscription tier, email domain)

  • Audit logs (compliance + debugging)

  • Analytics integration (measure impact)

  • Team permissions (who can change flags?) Recommended path:

  • Start: DIY flags (the code above)

  • When you hit: 50+ flags in production → Switch to Unleash (open-source, self-hosted)

  • When you need: Analytics + targeting → Switch to PostHog (includes analytics)

  • When you need: Enterprise compliance → LaunchDarkly (SOC 2, HIPAA)


Common Mistakes to Avoid

Mistake Impact Fix
Never clean up flags Codebase becomes unmaintainable Set a 2-week cleanup deadline
No metrics defined You don't know if rollout succeeded Define error rate + latency targets before launching
Rollout all at once Production incident risk is maximum Always start at 1%, double each stage
Complex flag logic Hard to reason about, bugs hide Keep flags simple (boolean or 2-3 variants max)
Nested flags Flag interdependencies create chaos Never have flag A depend on flag B

Your Competitive Edge

Teams shipping without feature flags:

  • Deploy once daily, wait 30–60 min per deploy
  • Production incidents = rollbacks + hotfixes + stress
  • Can't A/B test new features
  • Risk scales with team size Teams using feature flags:
  • Deploy 10x per day (code ready ≠ released)
  • Production incidents = toggle flag, done
  • Run A/B tests alongside rollouts
  • Incident recovery: 30 seconds vs 30 minutes The difference in shipping velocity is enormous.

Start This Week

  1. Copy the Redis code above into your app
  2. Wrap one new feature behind isFeatureEnabled('my_feature', userId)
  3. Deploy with flag OFF
  4. Roll out: 1% → 5% → 25% → 100%
  5. Watch your incident recovery time drop from 30 minutes to 30 seconds You'll never ship without flags again.

Next Steps

  • DIY approach: Use the code above; use Redis or your database
  • Lightweight SaaS: Try PostHog (free tier, includes analytics)
  • Self-hosted open-source: Deploy Unleash (Docker, <20 min setup)
  • Enterprise: LaunchDarkly (compliance, audit logs, support) Choose based on team size and complexity. Start with DIY. Migrate when needed.

Got stuck, or want this shipped end-to-end for you? bitroot.club builds custom products for founders. →