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:
{
"checkout.v2": {
"enabled": true,
"rolloutPercent": 5,
"cohorts": ["internal-staff"],
"owner": "billing-team",
"removalDate": "2026-10-01"
}
}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.
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:
- Test both paths. CI should run the suite once with the flag on and once off for every release-critical flag.
- 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:
create → rollout → verify → REMOVE
| | | |
owner, percentages, metrics code change:
expiry, cohorts vs baseline delete branch + flag
both paths 100% liveThe 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
nflags, the codebase has2ⁿ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.