The Runtime Theory
Software Architecture

Feature Flags and Release Engineering: Kill Switches, Rollouts, and Flag Debt

Feature flags decouple deployment from release — how flags work through their lifecycle, percentage rollouts and kill switches, and the debt flags create when nobody removes them.

The Runtime Theory Team3 min read#feature-flags#release-engineering#continuous-delivery#rollouts
On this page

Deployment and release are different events. Deployment is "the code is on the server"; release is "users can see the behavior." A feature flag is the mechanism that separates them: a runtime condition in the code that decides which behavior executes. That separation buys you the two most valuable properties in production — releasing without deploying and killing without rolling back — and it costs you exactly one thing in return: a flag must eventually be removed, or you accumulate debt that compounds. This article covers the mechanics, the lifecycle, and the accounting.

The mechanism

A flag is a key evaluated at runtime, typically fetched from a config server so the value can change without a redeploy:

json
{
  "checkout.v2": {
    "enabled": true,
    "rolloutPercent": 5,
    "cohorts": ["internal-staff"],
    "owner": "billing-team",
    "removalDate": "2026-10-01"
  }
}
typescript
if (flags.isEnabled("checkout.v2", user)) {
  return renderCheckoutV2(user);
}
return renderCheckoutV1(user);

That single if is a kill switch: when the flag flips off, the old behavior resumes within the config propagation interval — seconds, not a redeploy. The rollback story changes from "revert and redeploy a release" to "flip a value." That is the entire value proposition, and it is real.

Rollout percentages and stickiness

Percentage rollouts are how you release to a growing fraction of users. The naive implementation — hash(userId) % 100 < percent — works, but only if the hash is sticky: the same user must land on the same side every time the flag is evaluated, or users flip-flop between behaviors on every request.

typescript
function isEnabled(flag, userId) {
  const slot = (hash(flag.key + ":" + userId) % 100) + 1;
  return slot <= flag.rolloutPercent;
}

Include the flag key in the hash so changing one flag doesn't re-correlate everyone, and evaluate per user, not per request, when the behavior is user-visible. The rollout progression — 1%, 5%, 25%, 100% — is a monitoring schedule, not a ritual: each step is a checkpoint where you compare error rates and latency between the flagged and unflagged populations before widening.

Kill switches are only as good as their off-path

A kill switch that has never been exercised off is not a kill switch. Code paths hidden behind flags rot: the v1 branch stops receiving tests, its dependencies get removed, and the day the switch is flipped off in an incident, the "old" behavior is broken. Two disciplines keep switches honest:

  1. Test both paths. CI should run the suite once with the flag on and once off for every release-critical flag.
  2. Prefer "default on" for kill switches, "default off" for new features. A feature flag defaulted off means the feature is invisible until you remember to enable it — a classic forgotten-feature failure. A kill switch defaulted on means the new behavior is live until an incident demands the off path — which is exactly the situation that warrants a kill switch.

The lifecycle: flags are temporary by design

Every flag has four states, and the fourth is the one teams skip:

text
create   →  rollout   →  verify   →  REMOVE
  |            |            |          |
owner,     percentages,  metrics      code change:
expiry,    cohorts       vs baseline  delete branch + flag
both paths               100% live

The removal is not optional cleanup — it is the final state of the lifecycle, a separate code change with its own review and deploy. The rule that makes it stick: a flag without an owner and a removal date is a liability, and the config server should refuse flags that lack both.

Flag debt

Flag debt is the accumulation of flags that outlive their purpose. The costs are concrete:

  • Combinatorial explosion. With n flags, the codebase has 2ⁿ reachable states. Ten flags is 1,024 states; no test matrix covers that, and no engineer can reason about it. The "it's just an if" cost compounds geometrically.
  • Dead branches. The v1 path of a finished feature is executed code that nothing exercises. Dead code is not harmless; it carries dependencies, build time, and review burden.
  • Silent coupling. One flag reading another's cohort — "checkout.v2 only for users in payments.beta" — turns flags into a hidden dependency graph that only the original author understands.

The honest accounting

Flags buy: deploy/release decoupling, second-scale kill switches, and gradual exposure to real traffic. They cost: one extra code path per feature, one config entry, one removal commit, and the discipline to track both paths. Teams that succeed treat flags as a lifecycle with a scheduled end. Teams that fail treat them as permanent furniture — and discover the cost when the 47th flag's combination of states makes a production bug unreproducible in any test environment. Ship flags with an owner, a removal date, and a CI job that pages the owner when the date passes.