DevOps Deployment

Feature Flags &
Progressive Rollouts

Kill switches, gradual rollouts, A/B testing, and safe deployment patterns. Ship fast without breaking things.

📖 11 min read January 24, 2026

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
25%
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

CI/CD for Workers
Read more →
Monitoring & Observability
Read more →
Prompt Engineering
Read more →

Need Safe Deployment Systems?

We build deployment pipelines with zero-downtime rollouts.

Get Started