How to ship safely with flags while preventing stale toggles and hidden dead code.
A few years back I inherited a service with 214 feature flags. I counted. Some of them were guarding code that had shipped to everyone two years earlier; a handful pointed at branches that no longer existed; and exactly one — I am not exaggerating — was load-bearing in a way nobody on the team could explain. We were all a little afraid to touch it.
That codebase taught me something I now believe deeply: feature flags are one of the best tools we have for shipping safely, and one of the easiest to turn into a slow-motion disaster. The difference isn't the tooling. It's the discipline around it.
A feature flag, if you're new to the term, is just a switch in your code — a conditional that lets you turn a piece of behavior on or off without deploying again. That's the whole idea. The trouble is that switches are cheap to add and expensive to forget. Here's how I keep them from piling up.
Treat flags as temporary
Every flag needs an owner and an expiration date. If you cannot state when it should be removed, it is probably a config setting, not a rollout flag — and the two deserve different homes in your codebase.
I've started asking one question in code review whenever a new flag shows up: "When does this go away?" If the author can answer — "once we've watched it at 100% for a week," "after the migration finishes in March" — great, write that down right next to the flag. If the honest answer is "never, this is how we toggle the feature for enterprise customers," then it isn't a rollout flag at all. It's configuration, and it belongs in your settings system where it'll be treated as permanent on purpose.
The distinction matters because the two have opposite lifecycles. A rollout flag wants to die. A config setting wants to live. When you mix them, you lose the ability to look at a list of flags and know which ones are debt.
Separate release and experiment flags
Release flags reduce deployment risk. Experiment flags measure behavior. Mixing both goals in one toggle creates confusing telemetry — and I've watched teams make real product decisions off numbers that were quietly lying to them because of it.
Here's the trap. You build a flag to roll out a new checkout flow gradually, which is a release concern — you want to limit blast radius if something breaks. Then someone says, "Well, while it's at 50%, let's compare conversion." Now the same flag is doing two jobs. The rollout percentage is being chosen for safety reasons, not statistical ones, and your experiment population is whoever happened to be in the safe bucket. The data looks like an A/B test. It isn't one.
Keep them separate. A release flag answers "is this safe to turn on?" An experiment flag answers "is this better?" Different owners, different dashboards, different exit criteria. When you need both — and you often do — use two flags and be honest about which is which.
Automate cleanup
Use lint rules or CI checks to alert on stale flags. Removing dead branches keeps systems understandable, and "understandable" is worth more than almost anything else in a system you'll maintain for years.
This is the part teams skip, and it's the part that saves you. A flag that's been at 100% for thirty days is no longer protecting anything — it's just a fork in the code that every future reader has to mentally evaluate and discard. Multiply that by a few hundred and you get my old 214-flag service, where reading a single function meant holding a dozen dead conditionals in your head.
I like a simple, boring approach: tag each flag with its creation date, run a scheduled CI job that lists anything older than your agreed threshold, and route that list somewhere a human will actually see it. You don't need a fancy platform. You need the removal to be someone's job and the staleness to be impossible to ignore. The deletion PRs are some of the most satisfying I write — every one of them makes the codebase a little smaller and a little easier to reason about.
None of this is exotic. Owner, expiration, one job per flag, and a cleanup loop that runs without anyone remembering to run it. Do those four things and flags go back to being what they're supposed to be: a quiet way to ship with confidence, not a haunted graveyard you tiptoe around.
If you've got a service that's drifted toward the haunted end of the spectrum, don't try to fix all of it at once. Just start counting. Once you can see the pile, it gets a lot less scary. Good luck.