Feature Flags and Feature Management in Production: Gradual Rollouts, Kill Switches, and A/B Testing
Deploying code to production doesn't mean releasing features to users. Feature flags decouple deployment from release, enabling gradual rollouts, instant rollbacks, and controlled experimentation.
Executive summary
Deploying code to production doesn't mean releasing features to users. Feature flags decouple deployment from release, enabling gradual rollouts, instant rollbacks, and controlled experimentation.
Last updated: 3/15/2026
Executive summary
The most dangerous deployment pattern is coupling code deployment with feature release. When a new feature is deployed and immediately visible to all users, any bug, performance degradation, or user experience issue affects 100% of your user base instantly. Rolling back means a full code deployment, often with lead time measured in hours.
Feature flags decouple deployment from release. Code is deployed to production with features disabled behind flags, and features are released gradually through configuration changes—not code deployments. This enables gradual rollouts, instant rollbacks via configuration, controlled experimentation, and surgical feature deactivation without redeployment.
In 2026, feature flag management has evolved from simple boolean flags to sophisticated targeting rules, multivariate testing, and real-time performance monitoring. Organizations that mature feature flagging reduce incident blast radius, accelerate experimentation, and deploy with confidence.
The feature flag taxonomy
Flag types and use cases
| Flag Type | Use Case | Example | Rollback Strategy |
|---|---|---|---|
| Boolean | Enable/disable features | showNewCheckout | Flip flag to false |
| Multivariate | A/B testing variants | checkoutVariant: ["A", "B", "C"] | Change percentage allocation |
| Gradual rollout | Percentage-based release | newDashboard: 25% | Reduce percentage to 0% |
| Targeted | User-based targeting | betaAccess: [userIds...] | Remove from allow list |
| Dynamic config | Runtime configuration | maxCartItems: 50 | Adjust value |
| Kill switch | Emergency disable | externalAPI: true/false | Immediately disable |
Anti-patterns to avoid
1. Permanent feature flags:
typescript// Bad: Feature flag never removed
if (featureFlags.showNewCheckout) {
return <NewCheckout />;
} else {
return <OldCheckout />; // Dead code, never cleaned up
}Solution: Create feature flag cleanup as part of the release process. Flags should have an expiration date and documented removal plan.
2. Flag logic scattered across codebase:
typescript// Bad: Flag checks everywhere
if (featureFlags.newFeature) { /* ... */ }
if (featureFlags.newFeature) { /* ... */ }
if (featureFlags.newFeature) { /* ... */ }Solution: Centralize flag logic behind well-defined interfaces:
typescript// Good: Flag behind feature service
interface CheckoutFeature {
useVariant: () => 'new' | 'old';
isEnabled: () => boolean;
}
const checkoutFeature: CheckoutFeature = {
useVariant: () => featureFlags.newCheckout ? 'new' : 'old',
isEnabled: () => featureFlags.newCheckout
};Implementation patterns
Pattern 1: Gradual rollout
Gradually increase user exposure to new features:
typescriptinterface GradualRolloutConfig {
percentage: number; // 0-100
userIdentifier: string; // User ID or similar stable identifier
salt: string; // Ensures consistent assignment
}
function isInGradualRollout(config: GradualRolloutConfig): boolean {
const hash = crypto
.createHash('md5')
.update(`${config.salt}:${config.userIdentifier}`)
.digest('hex');
const hashNumber = parseInt(hash.substring(0, 8), 16);
const percentage = (hashNumber / 0xFFFFFFFF) * 100;
return percentage < config.percentage;
}
// Usage
function renderCheckout(user: User): JSX.Element {
const isInRollout = isInGradualRollout({
percentage: featureFlags.checkoutRollout, // e.g., 25%
userIdentifier: user.id,
salt: 'new-checkout-v1'
});
if (isInRollout) {
return <NewCheckout />;
}
return <OldCheckout />;
}Pattern 2: Targeted rollout
Enable features for specific users, segments, or attributes:
typescriptinterface TargetedRolloutConfig {
allowList: string[]; // User IDs
denyList: string[]; // User IDs (takes precedence)
rules: TargetingRule[];
}
interface TargetingRule {
attribute: string; // e.g., 'plan', 'region', 'email'
operator: 'equals' | 'contains' | 'matches' | 'in';
value: any;
}
function isInTargetedRollout(
user: User,
config: TargetedRolloutConfig
): boolean {
// Check deny list first
if (config.denyList.includes(user.id)) {
return false;
}
// Check allow list
if (config.allowList.length > 0) {
return config.allowList.includes(user.id);
}
// Check targeting rules
return config.rules.every(rule => {
const userValue = (user as any)[rule.attribute];
switch (rule.operator) {
case 'equals':
return userValue === rule.value;
case 'contains':
return userValue?.includes(rule.value);
case 'matches':
return new RegExp(rule.value).test(userValue);
case 'in':
return Array.isArray(rule.value) && rule.value.includes(userValue);
default:
return false;
}
});
}
// Usage
function renderPremiumFeature(user: User): JSX.Element {
const hasAccess = isInTargetedRollout(user, {
allowList: [], // No specific users
denyList: ['user123', 'user456'], // Explicitly deny
rules: [
{
attribute: 'plan',
operator: 'equals',
value: 'premium'
}
]
});
if (!hasAccess) {
return <UpgradePrompt />;
}
return <PremiumFeature />;
}Pattern 3: Multivariate testing (A/B/n)
Test multiple variants of a feature:
typescriptinterface MultivariateConfig {
variants: {
[key: string]: number; // variant name -> percentage
};
userIdentifier: string;
salt: string;
}
function assignVariant(config: MultivariateConfig): string | null {
const totalPercentage = Object.values(config.variants).reduce((a, b) => a + b, 0);
if (totalPercentage === 0) {
return null;
}
const hash = crypto
.createHash('md5')
.update(`${config.salt}:${config.userIdentifier}`)
.digest('hex');
const hashNumber = parseInt(hash.substring(0, 8), 16);
const percentage = (hashNumber / 0xFFFFFFFF) * 100;
let cumulative = 0;
for (const [variant, variantPercentage] of Object.entries(config.variants)) {
cumulative += variantPercentage;
if (percentage < cumulative) {
return variant;
}
}
return null;
}
// Usage
function renderHomepage(user: User): JSX.Element {
const variant = assignVariant({
variants: {
'control': 40, // 40% see control
'variant-a': 40, // 40% see variant A
'variant-b': 20 // 20% see variant B
},
userIdentifier: user.id,
salt: 'homepage-test-2026-03'
});
switch (variant) {
case 'control':
return <HomepageControl />;
case 'variant-a':
return <HomepageVariantA />;
case 'variant-b':
return <HomepageVariantB />;
default:
return <HomepageControl />; // Fallback
}
}Pattern 4: Kill switch for external dependencies
Instantly disable problematic external services:
typescriptinterface KillSwitchConfig {
enabled: boolean;
fallbackImplementation?: () => any;
circuitBreakerThreshold?: number;
circuitBreakerTimeout?: number;
}
class ServiceWithKillSwitch {
private circuitBreakerFails = 0;
private circuitBreakerOpenUntil: Date | null = null;
constructor(
private serviceName: string,
private config: KillSwitchConfig
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
// Check if kill switch is enabled
if (!this.config.enabled) {
if (this.config.fallbackImplementation) {
return this.config.fallbackImplementation();
}
throw new Error(`${this.serviceName} is disabled via kill switch`);
}
// Check circuit breaker
if (this.circuitBreakerOpenUntil && new Date() < this.circuitBreakerOpenUntil) {
throw new Error(`${this.serviceName} circuit breaker is open`);
}
try {
const result = await fn();
this.circuitBreakerFails = 0;
return result;
} catch (error) {
this.circuitBreakerFails++;
// Open circuit breaker if threshold reached
if (this.config.circuitBreakerThreshold &&
this.circuitBreakerFails >= this.config.circuitBreakerThreshold) {
this.circuitBreakerOpenUntil = new Date(
Date.now() + (this.config.circuitBreakerTimeout || 60000)
);
}
throw error;
}
}
}
// Usage
const paymentService = new ServiceWithKillSwitch('payment-api', {
enabled: featureFlags.paymentAPI, // Can be flipped instantly
fallbackImplementation: () => ({ status: 'maintenance_mode' }),
circuitBreakerThreshold: 5,
circuitBreakerTimeout: 300000 // 5 minutes
});
async function processPayment(amount: number): Promise<PaymentResult> {
return paymentService.call(() =>
externalPaymentAPI.charge(amount)
);
}Feature flag platforms
Platform comparison for 2026
| Platform | Strengths | Trade-offs |
|---|---|---|
| LaunchDarkly | Enterprise features, detailed targeting, SDKs | Cost, learning curve |
| Split.io | Strong experimentation tools, data science integration | Complexity for simple use cases |
| Flagsmith | Open source options, simple API | Fewer enterprise features |
| Unleash | Open source, self-hostable, enterprise tier | UI less polished than commercial |
| Statsig | Built-in analytics, generous free tier | Newer platform, smaller ecosystem |
When to use commercial vs. open source
Use commercial platform when:
- You have enterprise compliance requirements (SOC 2, HIPAA)
- You need sophisticated targeting and experimentation
- You have a large team managing multiple products
- You want built-in analytics and data science integration
Use open source when:
- Budget is constrained
- You have engineering capacity for self-hosting
- You need complete control over flag data
- You want to avoid vendor lock-in
Custom feature flag service
For organizations with specific requirements, a custom flag service may be appropriate:
typescript// Feature flag service interface
interface FeatureFlagService {
getFlag(name: string, context: FlagContext): Promise<FlagValue>;
getAllFlags(context: FlagContext): Promise<Record<string, FlagValue>>;
trackEvent(event: AnalyticsEvent): Promise<void>;
}
interface FlagContext {
userId?: string;
sessionId?: string;
attributes?: Record<string, any>;
environment: 'development' | 'staging' | 'production';
}
interface FlagValue {
type: 'boolean' | 'string' | 'number' | 'json';
value: any;
variation?: string;
}
// Redis-backed implementation
class RedisFeatureFlagService implements FeatureFlagService {
constructor(private redis: RedisClient) {}
async getFlag(name: string, context: FlagContext): Promise<FlagValue> {
const flagConfig = await this.redis.get(`flag:${name}`);
if (!flagConfig) {
return { type: 'boolean', value: false }; // Default disabled
}
const config = JSON.parse(flagConfig);
return this.evaluateFlag(config, context);
}
private evaluateFlag(config: any, context: FlagContext): FlagValue {
// Apply rollout rules
if (config.rollout) {
const isInRollout = isInGradualRollout({
percentage: config.rollout.percentage,
userIdentifier: context.userId || context.sessionId || 'anonymous',
salt: name
});
if (!isInRollout) {
return { type: 'boolean', value: false };
}
}
// Apply targeting rules
if (config.targeting) {
const isTargeted = isInTargetedRollout(
{ id: context.userId, ...context.attributes },
config.targeting
);
if (!isTargeted) {
return { type: 'boolean', value: false };
}
}
// Apply multivariate assignment
if (config.variants) {
const variant = assignVariant({
variants: config.variants,
userIdentifier: context.userId || context.sessionId || 'anonymous',
salt: name
});
return {
type: config.type,
value: variant,
variation: variant
};
}
return {
type: config.type,
value: config.value
};
}
async getAllFlags(context: FlagContext): Promise<Record<string, FlagValue>> {
const allFlagNames = await this.redis.keys('flag:*');
const flags: Record<string, FlagValue> = {};
for (const flagName of allFlagNames) {
const name = flagName.replace('flag:', '');
flags[name] = await this.getFlag(name, context);
}
return flags;
}
async trackEvent(event: AnalyticsEvent): Promise<void> {
// Send to analytics platform
await this.redis.lpush('analytics-events', JSON.stringify(event));
}
}
// Usage
const featureFlags = new RedisFeatureFlagService(redis);
async function renderFeature(featureName: string, user: User): Promise<JSX.Element> {
const flag = await featureFlags.getFlag(featureName, {
userId: user.id,
attributes: {
plan: user.plan,
region: user.region
},
environment: process.env.NODE_ENV
});
if (flag.value) {
return <NewFeature variant={flag.variation} />;
}
return <OldFeature />;
}Rollout strategies
Strategy 1: Canary deployment
Gradually expose feature to increasing percentage of users:
T0: 1% → Monitor for errors
T1: 5% → Monitor for errors and performance
T2: 10% → Monitor for errors, performance, and user feedback
T3: 25% → Monitor all metrics
T4: 50% → Monitor all metrics
T5: 100% → Full rollout completeImplementation:
typescriptasync function gradualRollout(
featureName: string,
targetPercentage: number,
durationMinutes: number
): Promise<void> {
const steps = [1, 5, 10, 25, 50, 100].filter(p => p <= targetPercentage);
for (const percentage of steps) {
await updateFeatureFlag(featureName, { rollout: { percentage } });
console.log(`Rolled out ${featureName} to ${percentage}%`);
// Wait and monitor
await sleep(durationMinutes / steps.length * 60 * 1000);
// Check for issues
const errorRate = await getErrorRate(featureName, percentage);
if (errorRate > ERROR_THRESHOLD) {
console.warn(`Error rate ${errorRate}% exceeds threshold, pausing rollout`);
await alertTeam(featureName, percentage, errorRate);
return; // Pause rollout
}
}
console.log(`Gradual rollout of ${featureName} complete`);
}Strategy 2: Blue-green rollout
Switch all users at once but with instant rollback capability:
Phase 1: Deploy code with feature disabled
└─ Flag set to 0%
└─ Validate deployment
Phase 2: Enable feature (100%)
└─ Flag set to 100%
└─ Monitor closely
Phase 3: If issues, disable instantly
└─ Flag set to 0%
└─ No code deployment neededStrategy 3: Targeted beta rollout
Enable for specific user segments:
typescriptasync function betaRollout(
featureName: string,
betaUsers: string[],
betaPlan: string
): Promise<void> {
await updateFeatureFlag(featureName, {
targeting: {
rules: [
{
attribute: 'plan',
operator: 'equals',
value: betaPlan
}
],
allowList: betaUsers
}
});
}A/B testing with feature flags
Experiment design
typescriptinterface Experiment {
name: string;
variants: {
[key: string]: any;
};
metrics: string[]; // Metrics to track
durationDays: number;
sampleSize: number;
}
function createExperiment(experiment: Experiment) {
// Create feature flag for experiment
return updateFeatureFlag(experiment.name, {
variants: Object.fromEntries(
Object.keys(experiment.variants).map(name => [name, 100 / Object.keys(experiment.variants).length])
)
});
}
async function analyzeExperimentResults(experiment: Experiment): Promise<ExperimentResults> {
const results: ExperimentResults = {
name: experiment.name,
variants: {},
winner: null,
significance: 0
};
for (const [variantName, variantConfig] of Object.entries(experiment.variants)) {
const metrics = await getMetricsForVariant(experiment.name, variantName);
results.variants[variantName] = {
config: variantConfig,
metrics,
users: await getUserCount(experiment.name, variantName)
};
}
// Statistical analysis
results.winner = determineWinner(results.variants);
results.significance = calculateSignificance(results.variants);
return results;
}Metric tracking
typescriptasync function trackMetric(
eventName: string,
variant: string,
user: User,
value?: number
): Promise<void> {
await analytics.track({
event: eventName,
properties: {
variant,
userId: user.id,
feature: 'experiment-name',
value
},
timestamp: new Date()
});
// Track in feature flag service
await featureFlags.trackEvent({
type: 'metric',
event: eventName,
variant,
value,
timestamp: new Date()
});
}
// Usage
function onClickCTA(variant: string, user: User) {
trackMetric('cta_clicked', variant, user);
}
async function onPurchase(variant: string, user: User, amount: number) {
await trackMetric('purchase_completed', variant, user, amount);
}Best practices and governance
Flag lifecycle management
- Planning phase:
- Document flag purpose and success criteria
- Define removal plan and expiration date
- Assign flag owner and reviewers
- Implementation phase:
- Use descriptive flag names
- Implement proper default values
- Add telemetry for flag usage
- Release phase:
- Start with low percentage rollout
- Monitor metrics at each step
- Document decisions and findings
- Cleanup phase:
- Remove flag code after full rollout
- Delete flag from flag service
- Document learnings for future flags
Flag naming conventions
# Boolean flags
enable[feature-name] (e.g., enableNewCheckout)
disable[feature-name] (e.g., disableLegacyAPI)
# Configuration flags
config[feature-name][parameter] (e.g., configCheckoutMaxItems)
# Experiment flags
experiment[test-name] (e.g., experimentHomepageCTA)
# Kill switch flags
kill[service-name] (e.g., killExternalPaymentAPI)Performance considerations
Cache flag evaluations:
typescriptclass CachedFeatureFlagService implements FeatureFlagService {
private cache = new Map<string, { value: FlagValue; expires: number }>();
private readonly CACHE_TTL = 60000; // 1 minute
constructor(private delegate: FeatureFlagService) {}
async getFlag(name: string, context: FlagContext): Promise<FlagValue> {
const cacheKey = this.getCacheKey(name, context);
const cached = this.cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.value;
}
const value = await this.delegate.getFlag(name, context);
this.cache.set(cacheKey, {
value,
expires: Date.now() + this.CACHE_TTL
});
return value;
}
private getCacheKey(name: string, context: FlagContext): string {
return `${name}:${context.userId}:${JSON.stringify(context.attributes)}`;
}
}Monitoring and observability
Key metrics to track
- Flag usage metrics:
- Number of users per variant
- Percentage of users seeing each flag state
- Flag flip frequency
- Business metrics:
- Conversion rates per variant
- Revenue impact
- User engagement metrics
- Technical metrics:
- Error rates per variant
- Performance metrics per variant
- Flag evaluation latency
- Operational metrics:
- Number of active flags
- Flag age distribution
- Flag cleanup completion rate
Alerting on flag issues
typescriptasync function monitorFlags(): Promise<void> {
const allFlags = await featureFlags.getAllFlags({ environment: 'production' });
for (const [flagName, flagValue] of Object.entries(allFlags)) {
// Check for stale flags (> 90 days old)
const flagAge = await getFlagAge(flagName);
if (flagAge > 90) {
await alertStaleFlag(flagName, flagAge);
}
// Check for flags at intermediate rollout percentages
if (flagValue.variation && flagValue.variation !== 'control' && flagValue.variation !== 'treatment') {
const rolloutPercentage = getRolloutPercentage(flagValue);
if (rolloutPercentage > 0 && rolloutPercentage < 100) {
await alertPartialRollout(flagName, rolloutPercentage);
}
}
// Check for high error rates
const errorRate = await getErrorRateForFlag(flagName);
if (errorRate > ERROR_THRESHOLD) {
await alertHighErrorRate(flagName, errorRate);
}
}
}Conclusion
Feature flags in 2026 have evolved from simple boolean switches to sophisticated release and experimentation platforms. When implemented correctly, they decouple deployment from release, enable safe experimentation, provide instant rollback capabilities, and reduce incident blast radius.
The mature organization treats feature flags as first-class artifacts: documented, versioned, and with defined lifecycles. Teams that mature feature flagging practices deploy with confidence, experiment systematically, and respond to incidents instantly without code deployments.
Practical closing question: How many feature flags are currently active in your production environment, and how many have cleanup plans documented?
Need to implement feature flag infrastructure or improve your production deployment practices? Talk to Imperialis about feature flag architecture, platform selection, and production-ready implementation.
Sources
- FeatureFlag.dev - Open source feature flag platform
- LaunchDarkly Documentation — Official documentation
- Split.io Experimentation Platform — Official documentation
- Unleash Documentation — Open source platform
- Progressive Delivery: Feature Flags and More - GitLab — Progressive delivery guide