The feature flag that never got removed
Flags are cheap to add and nobody owns removing them. A codebase with three hundred of them has 2^300 configurations, none of which is tested, and one of which is production.

Short answer
A feature flag is debt with an interest rate: every one doubles the configurations your code can be in, and none of the combinations is tested. Give each flag an owner and a removal date when it is created, distinguish release flags from permanent configuration, and treat removing them as part of shipping rather than a cleanup task nobody schedules.
On this page
Feature flags solve a real problem. Deploy and release stop being the same event, a change can be turned off without a rollback, and a risky path can be exposed to 1% of traffic first.
Then they accumulate, because adding one is a line of code and removing one is a decision nobody is asked to make.
The cost is combinatorial
Two flags mean four possible configurations. Ten mean 1,024. Three hundred mean a number with ninety digits, and your test suite covers exactly one path through it — whatever the defaults happen to be.
In practice this means:
- The combination in production may never have been run in CI. Flag states differ between environments, and the flags are set outside the code.
- Reproducing a bug requires knowing the flag state, which is not in the stack trace, the logs, or the ticket unless someone made it so.
- Reading the code becomes conditional. A new engineer cannot tell which branch is live, so they read both, and the flag has now cost them twice.
Two kinds, and conflating them is the mistake
Release flags are temporary. They exist to separate deploy from release, and they have a natural end: the feature is fully on, or it is deleted. Their lifetime should be days or weeks.
Configuration is permanent. Per-tenant limits, regional behaviour, plan entitlements, kill switches for expensive subsystems. These are not debt — they are the product, and they should live in a configuration system with an interface that says so.
Most flag sprawl is release flags that were never closed out, sitting in the same list as permanent configuration, which is why nobody can tell which are safe to remove.
What to require when a flag is created
Four fields, enforced by the flag system rather than by good intentions:
- An owner. A person, not a team alias. Teams do not clean things up.
- A removal date. Not a hope — a date, on which something happens automatically.
- The type. Release or configuration. This is the single most useful piece of metadata, because it decides whether the flag has an end.
- What it does when the system cannot reach the flag service. Every flag needs a default that is safe when evaluation fails, and choosing it at creation is much easier than choosing it during an incident.
Removing them is part of shipping
The flag comes out when the feature is fully rolled out — not in a cleanup sprint, which does not happen, and not in a backlog ticket, which is where flags go to become permanent.
A workable rhythm:
- The pull request that takes a flag to 100% includes the removal ticket, assigned, with a date.
- A weekly report of flags past their removal date, sent to the owner rather than a channel. Named individuals respond; channels do not.
- A hard limit on live release flags. A number the team agrees, past which new flags require removing an old one. Artificial, and it works — the alternative is an unbounded list nobody feels responsible for.
A flag with no removal date is not a flag. It is an untested branch of your codebase with a switch on the outside.
Removing one safely
- Confirm the state everywhere. Every environment, every tenant, every region. A flag at 100% in production and 0% in staging is not finished.
- Delete the losing branch and the flag check together, in one change. Leaving the check with a constant is how a flag survives its own removal.
- Search for the flag name across the whole repository, including tests, configuration, analytics events and dashboards. Analytics that segments on a removed flag reports nothing and nobody notices for months.
- Remove it from the flag service last, after the code is deployed. The reverse order means a window where the code asks for a flag that no longer exists — which is exactly when you discover what the missing-flag default was.
The one to keep
Kill switches for expensive or fragile subsystems are worth keeping permanently and worth testing deliberately: turn one off in a non-production environment on a schedule and confirm the system degrades the way you think it does.
An untested kill switch is not a safety mechanism. It is a belief about one, and you find out which during the incident it was meant for.
What is a feature flag?
A feature flag is a runtime condition that selects between 2 code paths without a deploy. That is its value — deploy and release stop being the same event — and also its cost, because both paths remain in the codebase and only one of them is exercised by any given run.
A feature flag is therefore debt with an interest rate. The interest is paid by whoever reads the code next.
Which kind of flag is this?
| Release flag | Permanent configuration | |
|---|---|---|
| Lifetime | Days to weeks | Indefinite |
| Ends with | Deletion of the losing branch | Never |
| Owner | The engineer shipping the feature | The team owning the subsystem |
| Belongs in | The flag service, with a removal date | A configuration system |
| Example | New checkout, behind a flag at 10% | Per-tenant limits, regional behaviour, kill switches |
Conflating these 2 is the root of most flag sprawl. Release flags that were never closed out end up sitting in the same list as genuine configuration, and once they are mixed nobody can tell which are safe to remove.
How many can a codebase carry?
It is not a count. It is the number of release flags past their removal date, because those are the ones representing untested combinations nobody intended to keep.
The 4 fields to require at creation: an owner who is a person, a removal date, the type, and the behaviour when the flag service cannot be reached. The last is the one chosen during an incident if it is not chosen at creation, and incidents are a poor time to make it.
Then treat removal as part of shipping — the pull request that takes a feature flag to 100% carries the removal ticket with it. See also our writing on DevOps practice, software engineering generally, and the product engineering decisions behind both.
Removing one without breaking anything
The order matters more than the care taken. Confirm the state in every environment and every tenant, delete the losing branch and the feature flag check together in one change, deploy, and only then remove it from the flag service. Reversing the last 2 steps leaves a window where the code asks for a flag that no longer exists, which is precisely when you discover what the missing-flag default was.
Search the whole repository for the name before you finish — tests, configuration, analytics events and dashboards. Analytics segmented on a removed feature flag reports nothing at all, and nobody notices for months. Trunk-based development treats this cleanup as part of the change rather than a follow-up, which is the only way it reliably happens.
Frequently asked questions
- How many feature flags is too many?
- It is not the count but the number of *release* flags past their removal date. Permanent configuration can be numerous and is fine; temporary flags that never ended are the debt.
- Why does a flag need a default for when the service is unreachable?
- Because evaluation can fail, and the behaviour then is a decision someone makes either at creation or during an incident. Making it at creation is considerably easier.
- What order do I remove a flag in?
- Delete the losing branch and the check together in one change, deploy, then remove it from the flag service. The reverse order leaves a window where the code asks for a flag that no longer exists.
- Should kill switches be removed too?
- No — those are permanent configuration and worth keeping. They should be exercised deliberately in a non-production environment, because an untested kill switch is a belief rather than a mechanism.
Sources
- Feature Toggles (aka Feature Flags) — martinfowler.com
- Accelerate: The Science of Lean Software and DevOps — IT Revolution
- Trunk Based Development — trunkbaseddevelopment.com
Published by
Tecno Blocks
Engineering insights from Tecno Blocks covering web, mobile, AI, Web3, software architecture, product development, DevOps, and real-world case studies.
About the publication
