Knowledge

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.

3/15/202610 min readKnowledge
Feature Flags and Feature Management in Production: Gradual Rollouts, Kill Switches, and A/B Testing

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 TypeUse CaseExampleRollback Strategy
BooleanEnable/disable featuresshowNewCheckoutFlip flag to false
MultivariateA/B testing variantscheckoutVariant: ["A", "B", "C"]Change percentage allocation
Gradual rolloutPercentage-based releasenewDashboard: 25%Reduce percentage to 0%
TargetedUser-based targetingbetaAccess: [userIds...]Remove from allow list
Dynamic configRuntime configurationmaxCartItems: 50Adjust value
Kill switchEmergency disableexternalAPI: true/falseImmediately 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

PlatformStrengthsTrade-offs
LaunchDarklyEnterprise features, detailed targeting, SDKsCost, learning curve
Split.ioStrong experimentation tools, data science integrationComplexity for simple use cases
FlagsmithOpen source options, simple APIFewer enterprise features
UnleashOpen source, self-hostable, enterprise tierUI less polished than commercial
StatsigBuilt-in analytics, generous free tierNewer 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 complete

Implementation:

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 needed

Strategy 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

  1. Planning phase:
  • Document flag purpose and success criteria
  • Define removal plan and expiration date
  • Assign flag owner and reviewers
  1. Implementation phase:
  • Use descriptive flag names
  • Implement proper default values
  • Add telemetry for flag usage
  1. Release phase:
  • Start with low percentage rollout
  • Monitor metrics at each step
  • Document decisions and findings
  1. 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

  1. Flag usage metrics:
  • Number of users per variant
  • Percentage of users seeing each flag state
  • Flag flip frequency
  1. Business metrics:
  • Conversion rates per variant
  • Revenue impact
  • User engagement metrics
  1. Technical metrics:
  • Error rates per variant
  • Performance metrics per variant
  • Flag evaluation latency
  1. 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

Related reading