Feature Flags Deployment Strategy: Gradual Rollouts, Kill Switches, and A/B Testing Integration

Published 14 min read
feature-flagsgradual-rollouta-b-testinglaunchdarklyunleashdeployment

Feature flags decouple deployment from release, letting you ship code to production without exposing it to users until you choose. A flag-driven deployment strategy enables gradual rollouts (1% to 100%), instant kill switches, targeted releases by user segment, and integrated A/B testing. This guide covers implementation patterns, platform comparison, and operational best practices.

The 2025 DORA State of DevOps report found that teams using feature flags deploy 4.7x more frequently with 72% fewer deployment-related incidents. Feature flags reduce mean time to recovery (MTTR) from 60 minutes (rollback deployment) to under 30 seconds (toggle flag off).

---

Core Feature Flag Patterns

Release Flags

Release flags hide incomplete features behind a boolean toggle. The code is deployed but inactive.

<h1>Python: Simple release flag</h1>

from feature_flags import get_flag

def get_dashboard_data(user_id: int):

data = get_standard_metrics(user_id)

if get_flag("new_analytics_widget", user_id=user_id):

data["analytics"] = get_advanced_analytics(user_id)

return data

// React: Conditional rendering with feature flag

function Dashboard() {

const showNewWidget = useFeatureFlag("new_analytics_widget");

return (

<div>

<StandardMetrics />

{showNewWidget && <AdvancedAnalytics />}

</div>

);

}

Gradual Rollout Flags

Percentage-based rollouts expose a feature to an increasing share of users.

import hashlib

def is_user_in_rollout(user_id: int, flag_name: str, percentage: int) -> bool:

"""Deterministic rollout based on user ID hash."""

hash_input = f"{flag_name}:{user_id}"

hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)

bucket = hash_value % 100

return bucket < percentage

<h1>Rollout schedule</h1>

<h1>Day 1: 1% (canary)</h1>

<h1>Day 2: 10% (early adopters)</h1>

<h1>Day 3: 50% (half traffic)</h1>

<h1>Day 5: 100% (full release)</h1>

Rollout PhasePercentageDurationSuccess Criteria

Canary1%24 hoursError rate < 0.1%, latency within 10%
Early adopters10%24-48 hoursNo regression in core metrics
Broad rollout50%24-48 hoursBusiness metrics stable
Full release100%PermanentAll metrics healthy

---

Kill Switches

Kill switches are feature flags designed for emergency disabling. They should be evaluable without network calls (cached locally) and default to "off" (safe state).

import time

class KillSwitch:

def __init__(self, flag_client):

self.client = flag_client

self._cache = {}

self._cache_ttl = 30 # seconds

def is_enabled(self, switch_name: str) -> bool:

"""Check kill switch with local cache fallback."""

cached = self._cache.get(switch_name)

if cached and cached["expires"] > time.time():

return cached["value"]

try:

value = self.client.get_flag(switch_name)

self._cache[switch_name] = {

"value": value,

"expires": time.time() + self._cache_ttl

}

return value

except Exception:

# On failure, return safe default (disabled)

return False

<h1>Usage</h1>

kill_switch = KillSwitch(flag_client)

def process_payment(order):

if not kill_switch.is_enabled("payments_enabled"):

raise ServiceUnavailable("Payments temporarily disabled")

return stripe.charge(order)

---

Platform Comparison

FeatureLaunchDarklyUnleash (Self-Hosted)Custom (Redis-Based)

Price$10/seat/monthFree (OSS)Infrastructure cost
Evaluation speedSub-ms (edge)1-5ms (server)1-5ms (Redis)
SDK support25+ languages15+ languagesBuild your own
A/B testingBuilt-inPluginBuild your own
Audit logBuilt-inBuilt-inBuild your own
Targeting rulesAdvanced (segments, rules)Basic (strategies)Custom logic
ScalabilityGlobal edge networkSingle server (scale yourself)Redis cluster
Setup time30 minutes2-4 hours1-2 weeks

Custom Redis-Based Feature Flags

import redis

import json

class SimpleFeatureFlags:

def __init__(self, redis_url="redis://localhost:6379"):

self.redis = redis.from_url(redis_url)

def set_flag(self, name: str, config: dict):

"""Set a feature flag configuration."""

self.redis.hset("feature_flags", name, json.dumps(config))

def get_flag(self, name: str, user_id: int = None) -> bool:

"""Evaluate a feature flag."""

raw = self.redis.hget("feature_flags", name)

if not raw:

return False

config = json.loads(raw)

if config.get("kill_switch") is False:

return False

if config.get("enabled") is False:

return False

percentage = config.get("percentage", 100)

if user_id and percentage < 100:

return is_user_in_rollout(user_id, name, percentage)

return True

<h1>Usage</h1>

flags = SimpleFeatureFlags()

flags.set_flag("new_checkout", {

"enabled": True,

"percentage": 10,

"kill_switch": True

})

---

A/B Testing Integration

def get_variant(user_id: int, experiment: str, variants: list) -> str:

"""Assign user to experiment variant deterministically."""

hash_input = f"{experiment}:{user_id}"

hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)

index = hash_value % len(variants)

return variants[index]

<h1>Experiment: test two checkout flows</h1>

variant = get_variant(user_id, "checkout_v2", ["control", "variant_a", "variant_b"])

if variant == "control":

render_original_checkout()

elif variant == "variant_a":

render_simplified_checkout()

else:

render_one_page_checkout()

<h1>Track conversion</h1>

analytics.track("checkout_completed", {

"experiment": "checkout_v2",

"variant": variant,

"user_id": user_id

})

For the CI/CD pipeline that deploys behind feature flags, see our GitHub Actions advanced workflows guide. Teams using canary deployments alongside feature flags should read our canary deployment best practices. For monitoring the impact of feature flag changes, check our distributed tracing guide.

---

Frequently Asked Questions

How do I manage technical debt from feature flags?

Every feature flag should have an owner and an expiration date. After a flag reaches 100% rollout and has been stable for 2 weeks, remove it. Track flag age in your backlog and create automatic tickets for flags older than 30 days. The average codebase accumulates 3-5 dead feature flags per quarter, each adding branching complexity and test surface area.

What is the performance impact of feature flag evaluation?

Locally cached flag evaluation takes under 1 microsecond. Server-side evaluation with a network call takes 1-5ms. Edge evaluation (LaunchDarkly, CloudFlare Workers) takes under 1ms. For hot paths (called 1000+ times per request), cache flags at the request level and evaluate once. Never evaluate flags inside loops.

How do I test code behind feature flags?

Test both flag states in your test suite. Create test helpers that set flag values deterministically. In integration tests, run the full test suite twice: once with the flag on and once with the flag off. This catches regressions in both the new and old code paths.

Can feature flags replace blue-green deployments?

Feature flags replace the release aspect of blue-green deployments but not the infrastructure aspect. Blue-green gives you two identical environments for instant traffic switching at the infrastructure level. Feature flags give you per-feature control at the application level. The most robust strategy uses both: blue-green for infrastructure rollback and feature flags for feature rollback.

How many feature flags are too many?

There is no hard limit, but operational complexity increases linearly with flag count. Teams with 10-20 active flags report manageable complexity. Above 50 active flags, teams consistently report confusion about flag interactions and difficulty testing all combinations. If you have more than 30 active flags, audit and remove stale ones before adding new ones.