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

StrengthsCost
LaunchDarklyBest UX, mature, expensivePremium
FlagsmithOSS-friendly, hostableFree OSS / paid SaaS
GrowthBookOSS, focused on experimentsFree OSS / paid
UnleashOSS enterprise, self-hostFree OSS / paid
PostHogIncludes product analyticsFree OSS / paid
Cloudflare Workers KVDIY for simple casesFree 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 .