DevOps
Deployment
Feature Flags &
Progressive Rollouts
Kill switches, gradual rollouts, A/B testing, and safe deployment patterns. Ship fast without breaking things.
Deploying code directly to production is gambling. Feature flags let you separate deployment from release. Deploy code anytime, but control who sees it and when—with an instant kill switch if things go wrong.
Here's how we ship features safely across 28 Workers serving production traffic.
Progressive Rollout: New Chatbot
1% (Day 1)
5% (Day 2)
25% (Day 3)
50% (Day 5)
100% (Day 7)
Pattern 1: Feature Flag Configuration
feature-flags.ts
interface FeatureFlag {
id: string;
enabled: boolean;
rollout_percentage: number; // 0-100
allowed_users?: string[]; // Explicit allowlist
allowed_tenants?: string[]; // Tenant-based access
environments?: string[]; // ['staging', 'production']
metadata?: Record<string, any>;
}
const FLAGS: Record<string, FeatureFlag> = {
'new-chatbot': {
id: 'new-chatbot',
enabled: true,
rollout_percentage: 25,
allowed_users: ['user_internal_1', 'user_internal_2'],
environments: ['production']
},
'ai-valuation-v2': {
id: 'ai-valuation-v2',
enabled: true,
rollout_percentage: 100,
environments: ['staging', 'production']
},
'experimental-ui': {
id: 'experimental-ui',
enabled: false, // Kill switch OFF
rollout_percentage: 0,
}
};
Pattern 2: Flag Evaluation
flag-evaluator.ts
class FeatureFlagService {
constructor(
private kv: KVNamespace,
private environment: string
) {}
async isEnabled(
flagId: string,
context: { userId?: string; tenantId?: string }
): Promise<boolean> {
// Get flag config (cached in KV)
const flag = await this.getFlag(flagId);
if (!flag || !flag.enabled) {
return false; // Kill switch
}
// Check environment
if (flag.environments && !flag.environments.includes(this.environment)) {
return false;
}
// Check explicit allowlist
if (context.userId && flag.allowed_users?.includes(context.userId)) {
return true;
}
if (context.tenantId && flag.allowed_tenants?.includes(context.tenantId)) {
return true;
}
// Percentage-based rollout
if (flag.rollout_percentage >= 100) {
return true;
}
if (flag.rollout_percentage <= 0) {
return false;
}
// Deterministic hash for consistent experience
const bucket = await this.getBucket(flagId, context.userId || 'anonymous');
return bucket < flag.rollout_percentage;
}
private async getBucket(flagId: string, userId: string): Promise<number> {
// Hash flag+user for consistent 0-99 bucket
const data = new TextEncoder().encode(`${flagId}:${userId}`);
const hash = await crypto.subtle.digest('SHA-256', data);
const view = new DataView(hash);
return view.getUint32(0) % 100;
}
}
| Flag | Status | Rollout | Use Case |
|---|---|---|---|
| new-chatbot | 25% | Progressive | Testing new AI chatbot |
| ai-valuation-v2 | 100% | Complete | Fully rolled out |
| experimental-ui | OFF | Killed | Caused issues, disabled |
| beta-features | Allowlist | Internal only | Testing with team |
Pattern 3: Kill Switch
usage-with-fallback.ts
async function handleChatRequest(request: Request, env: Env) {
const flags = new FeatureFlagService(env.KV, env.ENVIRONMENT);
const userId = getUserId(request);
const useNewChatbot = await flags.isEnabled('new-chatbot', { userId });
if (useNewChatbot) {
try {
return await newChatbotHandler(request, env);
} catch (error) {
// Auto-disable on repeated failures
await recordFailure('new-chatbot', error, env);
// Fall back to old chatbot
}
}
// Default: old chatbot
return await legacyChatbotHandler(request, env);
}
// Automatic kill switch based on error rate
async function recordFailure(flagId: string, error: Error, env: Env) {
const key = `flag-failures:${flagId}`;
const count = parseInt(await env.KV.get(key) || '0') + 1;
await env.KV.put(key, count.toString(), { expirationTtl: 300 });
if (count >= 10) {
// Auto-disable feature
await disableFlag(flagId, env);
await sendAlert(`🚨 Auto-disabled ${flagId} after ${count} failures`, env);
}
}
Rollout Strategy
Start at 1% for high-risk features. Monitor error rates and user feedback. Double the percentage each day if metrics are healthy. Always have a one-click kill switch ready.
Pattern 4: Admin API for Flag Management
flag-admin-api.ts
// POST /admin/flags/:id/rollout
async function updateRollout(request: Request, env: Env) {
const { flagId, percentage } = await request.json();
const flag = await getFlag(flagId, env);
if (!flag) throw new Error('Flag not found');
// Log the change
await logFlagChange({
flagId,
action: 'rollout_update',
before: flag.rollout_percentage,
after: percentage,
user: getAdminUser(request),
timestamp: new Date().toISOString()
}, env);
// Update flag
flag.rollout_percentage = percentage;
await env.KV.put(`flag:${flagId}`, JSON.stringify(flag));
// Notify team
await sendSlackNotification(
`📊 Flag "${flagId}" rollout changed: ${flag.rollout_percentage}% → ${percentage}%`,
env
);
return Response.json({ success: true, flag });
}
// POST /admin/flags/:id/kill
async function killFlag(request: Request, env: Env) {
const { flagId } = await request.json();
const flag = await getFlag(flagId, env);
flag.enabled = false;
flag.rollout_percentage = 0;
await env.KV.put(`flag:${flagId}`, JSON.stringify(flag));
await sendSlackNotification(`🔴 Flag "${flagId}" KILLED`, env);
return Response.json({ success: true });
}
Feature Flag Checklist
- Store flags in KV for sub-millisecond reads
- Use deterministic hashing for consistent user experience
- Support environment-specific flags (staging vs production)
- Implement allowlists for internal testing
- Log all flag changes with audit trail
- Build admin UI for non-engineers to manage flags
- Set up automatic kill switches on error spikes
- Clean up old flags—they become tech debt
Feature flags turn deployments from scary events into routine operations. Deploy anytime, release deliberately, rollback instantly.
Related Articles
Need Safe Deployment Systems?
We build deployment pipelines with zero-downtime rollouts.
→ Get Started