The first feature flag you add feels like a pure win. You wrap a risky change in an if, ship it turned off, flip it on for yourself, then for 5% of users, then everyone — and if it goes wrong you flip it back without a redeploy. Decoupling deploy from release is genuinely one of the best moves in modern shipping. I’m not here to talk you out of it.
I’m here to name the part of the deal nobody writes down: a flag is a loan. You borrow the ability to ship safely now, and you promise to pay it back later by removing the flag once the feature has won. The borrowing is a two-line change. The repayment is a chore with no deadline, no user asking for it, and no dopamine when it’s done. So it doesn’t happen. And an un-repaid flag doesn’t sit there neutrally — it accrues interest.
The interest comes in a shape worth staring at. Every live flag doubles a path through your code: on and off. Two independent flags give you four combinations. Ten flags give you 1,024. You do not test 1,024 combinations. You test the two or three you actually run, and you hope the rest are fine — which means most of the states your code can technically be in have never once been executed on purpose. A flag you meant to keep for a week, still live a year later, is now a branch that some new engineer has to reason about every time they touch that file, with no memory of why it exists or whether it’s safe to delete.
It helps to notice that “feature flag” is really several different tools wearing one name, because they have completely different lifespans.
A release flag is the short-lived kind — the one you use to roll a feature out gradually. Its whole purpose is to die. The day the feature is at 100% and stable, that flag is pure debt, and every day it lives past that is a day of interest.
A config or ops flag — a kill switch for an expensive subsystem, a regional toggle — is meant to live. That’s not debt; that’s an operational control. The mistake is treating the two the same and letting release flags overstay by pretending they’re config.
An experiment flag (the A/B kind) has a built-in expiry: the experiment ends, a winner is chosen, the flag comes out. When those linger, you get the worst version — code paths kept alive for a test that concluded months ago and that no one remembers the result of.
⁂
The fix isn’t “use fewer flags.” It’s to make repayment a scheduled event instead of a good intention. A few practices that actually hold.
Give every short-lived flag an owner and an expiry date at birth — written down where you’ll see it, not in someone’s head. A flag with no owner is a flag no one will ever dare delete.
When you flip a release flag to 100%, open the removal ticket in the same breath. The moment the feature has won is the moment its flag became debt; that’s the natural trigger, and it’s the one everyone lets slip because the feature is working and attention has moved on.
Audit the flag list on a cadence. A standing list of live flags, reviewed even monthly, turns “why does this exist?” from an archaeology dig into a five-minute conversation while someone still remembers.
And treat stale-flag removal as real work with real risk, not cleanup you squeeze in on a Friday — because deleting a branch that’s silently been holding the system together is exactly how you find out it wasn’t dead.
The honest framing is the same one that makes any debt manageable: it’s not that borrowing is bad. Flags let you ship in a way that would be reckless otherwise, and that’s worth a lot. It’s that a loan you never plan to repay isn’t leverage — it’s just a slow leak in the one thing you can least afford to lose in a growing codebase, which is the ability to look at a piece of code and know what it does.
Liked this? Get the next one in Working Theory.
Going weekly in August (it's in beta now). One genuinely interesting read on building, the brain, and the science most people missed.