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 completeIf 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
- Copy the Redis code above into your app
- Wrap one new feature behind
isFeatureEnabled('my_feature', userId) - Deploy with flag OFF
- Roll out: 1% → 5% → 25% → 100%
- 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. →