Server

Feature Flags in Practice

By Prabath Thalangama· October 24, 2026· 4 min read
#devops#feature-flags#release

Introduction

A feature flag is a runtime switch that changes behaviour without a deploy. It's what makes trunk-based development and progressive delivery work: merge incomplete code dark, turn it on when ready, turn it off instantly if it breaks.

Four types (they have different lifecycles)

  • Release flags — hide in-progress work; on when the feature ships. Short-lived — remove within weeks of full rollout.
  • Ops / kill switches — turn off an expensive or fragile feature under load or during an incident. Long-lived by design.
  • Experiment flags — A/B tests; assign users to variants, measure. Removed when the experiment concludes.
  • Permission / entitlement flags — gate features by plan, role, or customer. Permanent — really configuration, not a "flag" in the temporary sense.

Mixing lifecycles is where flag debt comes from — track the type and an expiry.

Where to evaluate

  • Server-side — the flag SDK evaluates in your backend; the client never sees flags for features it can't access. Preferred for anything security- or cost-sensitive.
  • Client-side — the browser/app fetches flag values. Fine for UI toggles; never gate access to data this way (the user can flip it).
  • Edge / config — evaluated at the CDN or in a config file for infrastructure-level flags.

Targeting and gradual rollout

flag: new-checkout
  default: off
  rules:
    - if user.plan == "enterprise" then on
    - if user.id in [beta-testers] then on
    - rollout: 5% of users (sticky by user.id hash)   # 5 -> 25 -> 50 -> 100

Sticky assignment (hash the user ID) matters: a user shouldn't flip between old and new on every request. The same hash also lets experiment flags keep a user in one variant.

Kill switches

Every risky feature gets an ops flag. During an incident:

flagctl set expensive-recommendations off --reason "INC-1234, CPU saturation"

No deploy, effective in seconds. Wire it so the fallback path is tested — a kill switch that leads to an untested code path is a second outage.

Flag debt and cleanup

Flags accumulate. A stale release flag is:

  • a branch in the code nobody exercises (rot),
  • a config-explosion risk (2^N combinations),
  • confusing during incidents ("is this flag on? what does it do?").

Practices:

  • Name flags with an owner and a created-date (checkout.new_flow.2026q3).
  • Track expiry — the flag system or a linter flags overdue ones.
  • A "clean up flag X" ticket created when the flag is created.
  • Percentage-100 for N weeks → remove the flag and the old code path as a routine chore.
  • Some teams run a scheduled job that lists flags older than 90 days for review.

Testing code behind flags

  • Test both paths — old and new — until the old one is removed. Parametrise tests over the flag value.
  • In CI, run the suite with the flag both on and off (or at least the affected tests).
  • Integration/E2E: a way to force a flag value per test (a header, a test-only override).
  • Don't let the flag default drift — the default should be the safe/current behaviour until rollout completes, then flip the default, then remove.

Build vs buy

  • Buy (LaunchDarkly, Flagsmith, Unleash-hosted, Statsig) — targeting UI, audit log, SDKs, streaming updates, experimentation stats. Worth it past a handful of flags.
  • Open source self-host — Unleash, Flagsmith, Flipt, GrowthBook.
  • DIY — a config file / DB table + a small SDK. Fine for < ~10 simple flags; you'll outgrow it (targeting, audit, gradual rollout, real-time updates).

Verification and troubleshooting

  • A user reports the feature is on/off inconsistently — non-sticky rollout (hash the user, not the request), or client-side + server-side flags disagreeing, or SDK cache staleness. Check the evaluation logs (good flag systems log every evaluation with the reason).
  • Flag flip had no effect — SDK not receiving updates (streaming connection down, falling back to a stale poll), the code reads the flag once at startup instead of per request, or you flipped it in the wrong environment.
  • Kill switch didn't help — the fallback path was broken/untested, or the flag only gates the new code and the shared code is the problem.
  • Combinatorial bugs — two flags interact. Reduce concurrent in-flight flags; remove flags promptly.
  • Flag config is a single point of failure — the SDK must fail safe to a bundled default if the flag service is unreachable; verify that behaviour.
  • Experiment results look wrong — assignment not sticky, sample ratio mismatch (SRM — one variant getting more traffic than configured), or peeking at results before the test concludes.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.