Feature Flags Are Debt With an Expiry Date

Key takeaway: A feature flag is a branch in production. Ten forgotten flags describe 1,024 possible states, almost none of which anyone has tested.
The Real Benefit
Feature flags separate deployment from release. Code ships dark, gets enabled for internal users, then a small cohort, then everyone. If metrics degrade, the flag flips off without a rollback, a rebuild or a redeploy.
That is a genuine reliability improvement, and it enables trunk-based development by removing the need for long-lived branches. The value is not in dispute.
The Cost Nobody Tracks
Every flag introduces a conditional, and conditionals multiply. Three flags produce eight combinations. Ten produce 1,024. Your test suite covers the two paths anyone thought about — all-on and all-off — and the remaining combinations exist in production untested.
The failure mode is specific: two flags that are individually correct interact badly. Flag A changes how a record is written. Flag B changes how it is read. Each was tested in isolation against the old behaviour of the other. The combination corrupts data, and it only appears for the subset of users who happen to have both enabled.
Meanwhile the code accumulates dead branches. A flag enabled for everyone six months ago still has an else clause that no request has reached since. That clause is untested, unmaintained, and will be read by someone who assumes it is live.
Distinguishing Flag Types
Not every flag is temporary, and treating them uniformly is part of the problem.
| Type | Purpose | Expected lifetime |
|---|---|---|
| Release flag | Ship dark, roll out gradually | Days to weeks |
| Experiment flag | A/B test | Duration of the test |
| Operational kill switch | Shed load, disable a dependency | Permanent |
| Entitlement flag | Plan or tier gating | Permanent |
Only the first two are debt. Kill switches and entitlement gates are legitimate long-lived configuration and belong in a different mechanism with different naming, so nobody confuses them with cleanup candidates.
Making Removal Happen
Cleanup does not occur through good intentions. It occurs through mechanisms.
Record an owner and an expiry date at creation. A flag without both should fail code review.
Fail the build on expiry. A test that reads flag metadata and errors when a release flag passes its date converts an invisible chore into a blocking one. This single mechanism does more than any amount of process advocacy.
Cap the count. Set an explicit ceiling on live release flags. Reaching it means removing one before adding another, which forces the trade-off into the open.
Remove the flag and the dead branch together. Deleting the conditional while leaving both code paths in place accomplishes nothing.
The Bottom Line
Use flags freely for their intended purpose, then treat removal as part of the feature rather than as follow-up work. Separate permanent operational switches from temporary release flags, attach an expiry to every temporary one, and enforce it in CI where it cannot be politely ignored.


