Feature flags decouple deploy from release. Done well: ship Friday afternoon, roll out Monday morning. Done poorly: a graveyard of forgotten flags, two-thousand-line config files, and “is this even on?” Slack threads. This post is the working playbook.
Flag types
Release flags: turn on a new feature gradually. Removed when feature is stable.
Kill switches: instantly disable a problematic feature. Long-lived but hopefully rarely hit.
Experiment flags: A/B test variants. Removed when experiment ends.
Permission flags: gate by user / tier / region. Long-lived; treat as config.
Ops flags: tune internal behavior (cache TTLs, rate limits) without deploys.
Distinguish — they have different lifecycles.
OpenFeature
The vendor-neutral standard:
from openfeature import api
api.set_provider(GrowthBookProvider(api_key="..."))
client = api.get_client()
if client.get_boolean_value("new-checkout", False, ctx):
new_flow()
else:
legacy_flow()
Swap providers (LaunchDarkly → GrowthBook → Flagsmith) without code changes. The standard ergonomics + interface let you migrate.
Evaluation context
ctx = EvaluationContext(
targeting_key=str(user.id),
attributes={
"email": user.email,
"tier": user.tier,
"country": user.country,
"client_version": req.headers.get("X-Client-Version"),
},
)
show_new = client.get_boolean_value("new-checkout", False, ctx)
Flags evaluate against rich context. Allows targeting:
- “5% of users.”
- “Pro tier in EU.”
- “Mobile clients ≥ 2.3.0.”
Rollout strategies
- 1% canary for 24h
- 10% for 24h, alarms green
- 50% for 24h
- 100%
- Remove flag in next release
Gradual exposes problems early; alarms catch regressions before mass impact.
Kill switch pattern
if client.get_boolean_value("payment-circuit-broken", False, ctx):
return error("Payments temporarily unavailable")
charge_user(...)
When fraud or downstream outage hits: flip the switch in seconds. No deploy. The kill switch is one of the highest-ROI patterns; build it in for any external dep.
Server-side vs client-side
Server-side: secure (flag values not exposed); evaluated in your code.
Client-side: snappy UX (flag determined before render); flags are visible (don’t put secrets).
Use server-side by default. Client-side only when latency / UX demands it.
Per-request evaluation
@app.middleware("http")
async def attach_flags(req, call_next):
req.state.flags = client.get_object_value("active-flags", {}, get_ctx(req))
return await call_next(req)
# Anywhere downstream:
if request.state.flags.get("new-checkout"):
...
Evaluate once per request; cache in request state. Saves provider calls per branch.
Caching and offline mode
provider = GrowthBookProvider(
api_key="...",
cache_ttl_seconds=60,
offline_fallback=True, # use last-known if provider unreachable
)
Provider outage shouldn’t take you down. Cache + fallback always.
Flag hygiene
The graveyard problem: flags stay forever; codebase rots.
Discipline:
- Naming:
<owner>.<feature>.<purpose>—checkout.new-flow.rollout. - Expiry tag on creation — review at expiry.
- Owner: every flag has one. Stale flags + departed owner = removal.
- Quarterly cleanup: list flags with
enabled=true, 100% rolled out, age > 90d. Remove from code; delete from provider. - Static analysis: lint to detect always-true flag branches.
Testing with flags
@pytest.fixture
def flags():
return {"new-checkout": True}
def test_new_checkout(client, flags, monkeypatch):
monkeypatch.setattr("flags.client", FakeClient(flags))
resp = client.post("/checkout", json={...})
assert resp.status_code == 200
Fake the provider in tests; toggle per test. Avoid hitting the real provider in unit tests.
Tools comparison
| Strengths | Cost | |
|---|---|---|
| LaunchDarkly | Best UX, mature, expensive | Premium |
| Flagsmith | OSS-friendly, hostable | Free OSS / paid SaaS |
| GrowthBook | OSS, focused on experiments | Free OSS / paid |
| Unleash | OSS enterprise, self-host | Free OSS / paid |
| PostHog | Includes product analytics | Free OSS / paid |
| Cloudflare Workers KV | DIY for simple cases | Free at small scale |
For new teams: GrowthBook + OpenFeature. Self-host for free, swap to paid if you need enterprise features.
Common mistakes
1. Flags as forever-config
Permission tier checks ≠ release flags. Don’t mix them — they have different review cadences.
2. Flag depth
if a:
if b:
if c:
new_flow()
Six flags deep. Refactor to feature branches once stable.
3. No caching
Every request hits the provider’s API. Slow, fragile.
4. Provider down → app down
Always have local fallback. Cache plus default values.
5. Secrets in flags
Flag values are not secrets. Don’t store API keys in flags.
What I’d ship today
For a new team:
- GrowthBook self-hosted (or LaunchDarkly if budget supports).
- OpenFeature SDK so providers are swappable.
- Kill switches for every external dependency.
- Quarterly cleanup scheduled.
- Flag review in code review (new flag: who’s the owner? when does it expire?).
Read this next
If you want my OpenFeature + GrowthBook starter, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .