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 Phase | Percentage | Duration | Success Criteria |
| Canary | 1% | 24 hours | Error rate < 0.1%, latency within 10% |
| Early adopters | 10% | 24-48 hours | No regression in core metrics |
| Broad rollout | 50% | 24-48 hours | Business metrics stable |
| Full release | 100% | Permanent | All 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
| Feature | LaunchDarkly | Unleash (Self-Hosted) | Custom (Redis-Based) |
| Price | $10/seat/month | Free (OSS) | Infrastructure cost |
| Evaluation speed | Sub-ms (edge) | 1-5ms (server) | 1-5ms (Redis) |
| SDK support | 25+ languages | 15+ languages | Build your own |
| A/B testing | Built-in | Plugin | Build your own |
| Audit log | Built-in | Built-in | Build your own |
| Targeting rules | Advanced (segments, rules) | Basic (strategies) | Custom logic |
| Scalability | Global edge network | Single server (scale yourself) | Redis cluster |
| Setup time | 30 minutes | 2-4 hours | 1-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.