Stale feature flags: how to find them and remove them safely
Old feature flags turn into dead code and hidden risk. How to spot stale flags, remove them without breaking production, and stop them from piling up.
Every release flag has a natural end: the feature reaches 100%, stays stable, and the flag stops being useful. If nobody removes it, it becomes a stale flag: a condition that always evaluates to the same value, guarding a code path that never runs.
One stale flag is harmless. Hundreds of them are a real problem.
Why stale flags hurt
- Dead code you still maintain. The "off" branch is never executed, but it still gets read, refactored and sometimes fixed.
- Hidden risk. Someone flips an old flag thinking it is unused, and code that has not run in a year goes live.
- Combinations nobody tests. With n flags there are up to 2ⁿ paths through the code.
- Slower onboarding. New engineers cannot tell which branches matter.
How to find stale flags
A flag is probably stale when one or more of these are true:
- It has served the same value to everyone for weeks. Your flag tool's evaluation data shows this directly.
- It has not been changed in a long time and it is a release flag, not a permission or ops flag.
- Its key is no longer evaluated. The flag exists in your tool but no running code asks for it, so it can be deleted from the tool.
- Its key is in the code but not in the tool. The SDK returns the default value, so the code behaves as if the flag were permanently off.
Points 3 and 4 need a link between your flag tool and your codebase. Searching the repository for each flag key is the simplest way to get one:
git grep -n "new-checkout" -- '*.ts' '*.tsx'
Removing a flag safely
Remove a flag in this order:
- Confirm the final value. Check that it really serves the same value to every user in every environment.
- Remove it from the code first. Replace the condition with the winning branch, delete the losing branch, and delete tests that only covered the losing path.
- Deploy and wait. Make sure no running version of your app still evaluates the flag. Remember mobile apps, where old versions stay installed for months.
- Archive the flag in your tool, then delete it later.
Doing it the other way round, deleting the flag in the tool while code still asks for it, makes the SDK return its default, which is often false. Your fully launched feature disappears.
Automate the boring part
Removing a flag from code is mechanical: replace isEnabled('x') with true, simplify the if, delete the dead branch. That makes it a good job for tooling. Uber published Piranha, an open-source tool that rewrites code to remove stale flags and opens the change for review. Today, coding agents can do the same job across languages when given the flag key and its final value. In both cases a human reviews the change before it merges.
Stop flags from piling up
- Give every flag an owner and an expected removal date when you create it.
- Mark the kind of flag. Release and experiment flags are temporary; permission and ops flags may be permanent. Only temporary ones should raise alerts.
- Make cleanup part of "done". A feature is not finished until its release flag is gone.
- Review the list regularly, for example once per sprint, sorted by age.
- Cap the number of temporary flags per team. A limit forces cleanup before new flags are added.
Flags make releases safer only when they are temporary. The cleanup is part of using them.