Blog · · 9 min read

Feature flag best practices: naming, ownership, and killing flag debt

Two of the four flag types are supposed to live forever, which is why "delete all the old flags" is advice that breaks production. Here is the system that works.

Feature flag best practices come down to six rules: name every flag so the name tells you its type and expected lifetime, give it an owner and an expiry date at creation, make the OFF path the safe path, never nest one flag inside another, test both branches while the flag is live, and delete release toggles the moment the rollout finishes. Ops and permission toggles are the exception. Those are supposed to stay.

What follows is what those rules look like in practice, written from the far side of a cleanup: a few hundred flags, no convention, no owners, and a wiki page nobody trusted. That mess was not carelessness. It was a team applying one policy to four different kinds of flag.

What are feature flag best practices?

Treat a flag as a small piece of infrastructure with a name, an owner, a type, and a lifetime, not as a throwaway boolean. Flags go bad when nobody can answer three questions: who owns this, what does OFF do, and when does it go away. Force those answers at creation time and most flag problems never happen.

The four kinds of flag, and why one cleanup policy cannot work

This taxonomy is the mental model that makes the rest of the article work. It is widely used across the industry (Martin Fowler's toggle categories are the usual reference point), and it matters for one practical reason: two of the four kinds are supposed to be permanent. If your policy is "delete anything older than 90 days," you will eventually delete a circuit breaker, and you will find out during an incident.

KindExampleExpected lifetimeCleanup policy
Release toggle release.checkout.new-flow Days to weeks Delete once the rollout hits 100% and soaks. An expired one is a bug.
Experiment toggle exp.pricing.annual-default Life of the test (2 to 8 weeks) Delete when the test concludes and the winner ships.
Ops toggle / kill switch ops.search.rate-limiter-v2 Long-lived on purpose Do not delete. Document it, drill it, review it. This is infrastructure.
Permission / entitlement toggle perm.billing.advanced-analytics Effectively permanent Do not delete. This is product configuration, not debt.

Nearly all flag debt is release toggles that outlived their rollout. Experiment toggles are the second pile, and they rot in a specific way: the test concluded, someone set the winner to 100%, and the losing branch is still sitting in the file. Ops toggles and entitlements are not debt at all, and every time a team lumps them in with the rest, the cleanup stalls, because nobody wants to be the engineer who deleted the load-shedding switch.

So the first move in a messy codebase is not to delete anything. It is to classify. Once every flag has a kind, the policy for it is obvious and the ambiguity disappears.

How should you name feature flags?

Name flags kind.domain.thing: lowercase, dots between segments, hyphens inside them. The first token is the kind (release, exp, ops, perm), the second is the owning domain, the third describes the change. The name should tell you the flag's expected lifetime before you open a single file.

So: release.checkout.new-flow, exp.search.semantic-ranking, ops.notifications.disable-digest-job, perm.billing.sso. Read all four and you already know which is safe to delete next month and which is load-bearing. A flag called newCheckout2 tells you nothing, and it will still be there in two years because nobody can prove it is dead.

Two supporting rules. Be ruthlessly consistent about case and separators, because Checkout.NewFlow and checkout.new-flow as two keys in one system is an incident waiting to happen. And name what turns ON, never what turns OFF: release.checkout.disable-legacy-override is a double negative someone will misread at 3am. If you already have hundreds of bad names, skip the big-bang rename. Apply the convention from today and rename old flags as you touch them.

Every flag needs an owner and an expiry date, at creation

A flag with no owner is a flag nobody will ever dare delete. That is the mechanism behind flag debt, and it is a social problem before it is a technical one. Engineers are correctly unwilling to rip a conditional they do not understand out of code they do not own, so orphaned flags pile up while everyone quietly agrees not to touch them.

The fix is four mandatory fields at creation: owner (a named person or team, not "platform"), kind, created date, and expected removal date. The last one is required for release and experiment toggles, and explicitly set to "permanent" for ops and permission toggles, which should be a decision someone signs off on rather than a default.

Then treat an expired release toggle as a bug, not a chore. Put it on a report the owner actually sees, open a ticket automatically, or fail a soft check in CI once a release flag is 30 days past its removal date. An expired flag sitting in a dashboard nobody opens is the same as having no policy.

The default value must be the safe path

Every flag read needs an explicit default, and that default has to be the behavior you already trust. If the flag service is unreachable, if the key was typo'd, if the SDK is still warming up on a cold start, the code should land on the path that worked yesterday.

// If the flag service is unreachable or the key is missing, this
// returns false and we serve the checkout that has worked for a year.
const useNewCheckout = await toggled.isEnabled('release.checkout.new-flow', {
  userId: user.id,
  defaultValue: false,
});

if (useNewCheckout) {
  return renderNewCheckout();
}

return renderLegacyCheckout();

Which leads to the rule that matters most at 2am: never invert a flag's polarity. ON means the new thing is on. OFF means the old, known-good thing. Always, across every flag in the system. The moment even one flag exists where OFF enables new code, "turn it off" stops being a safe instruction, and the on-call engineer has to reason about each flag individually during the worst ten minutes of their week. They will get one wrong. This is the same discipline that makes a kill switch trustworthy, covered in depth in the kill switches post.

Do not nest flags

Two flags that gate each other produce four states, and your team tests two of them. Three nested flags produce eight. Nobody writes that matrix, and the state that breaks production is always the one where the outer flag is on for 30% of users and the inner one for 10% of a different hash bucket. Wanting a second flag inside the first is a signal: the first should have shipped already. Finish that gradual rollout, delete the flag, start the next change from a clean base. If both changes truly must ship independently, use sibling flags at the same level.

What is feature flag debt?

Feature flag debt is the accumulated cost of live flags that should have been deleted. Every stale release toggle is a permanent branch where both paths have to keep working, keep passing tests, and keep being understood by the next engineer. The cost is not the if statement. It is the untested combinations and the fear of touching any of it.

The failure mode is specific. Someone changes a shared function used by both branches of a two-year-old flag. They test the branch live for 100% of users. The other branch, still technically reachable, breaks. Months later somebody flips that flag looking for a quick fix, and the "safe" path is now the broken one. Stale flags decay, and they take your escape hatch with them.

When should you remove a feature flag?

Remove a release toggle once it has sat at 100% (or 0%) with no incidents for a soak period, typically one to two weeks. Remove an experiment toggle the day the test concludes and the winner ships. Never remove an ops toggle or a permission toggle. The trigger is the flag's kind plus time at a terminal value, not a date on a calendar.

Should you delete old feature flags?

Yes, if they are release or experiment toggles. No, if they are ops or permission toggles. This is why "delete every flag older than N days" is bad advice: applied literally, it strips out your circuit breakers and your plan gating along with the debt. Classify first, then delete by kind.

The cleanup workflow, run weekly

Make it routine and boring. Once a week, someone (rotate it) pulls the flags parked at 100% or 0% for more than two weeks, checks the kind on each, and for the release and experiment toggles: removes the losing branch, drops the conditional, deletes the flag, closes the ticket. Ten minutes, most weeks. The mechanical half of that, finding every reference to a flag key across the repo and stripping the dead branch without changing behavior, is exactly the kind of sweep you can hand to an AI coding agent that works across the whole codebase and have it open the PR for review.

What kills teams is the quarterly purge: forty flags at once, one enormous PR, eleven services, reviewed by someone who was on none of the original rollouts. It gets postponed, then postponed again. A boring weekly habit beats a heroic annual one.

How many feature flags is too many?

There is no universal number, but there is a better metric than the total. Count only your live release and experiment toggles and compare that to the changes your team is actively rolling out. If ten engineers are carrying 60 open release toggles, that is six unfinished rollouts each, and it is far too many. Healthy is roughly the number of features in flight.

Ops and permission toggles do not count against you. A mature product can carry dozens of entitlement flags and a dozen circuit breakers and be in excellent health. Counting total flags is how teams feel guilty about the ones they should be proud of.

Test both paths while the flag is live

Any release toggle live in production needs both branches covered by tests. Not exhaustively, but enough that a change to shared code cannot silently break the OFF path. Parameterize the suite over the flag value for the flows the flag touches, and keep the old path green until the day you delete it. Ops toggles need something stronger than tests: rehearsal. Flip your kill switches on purpose, during business hours, when nothing is on fire. A circuit breaker that has never been pulled is a hypothesis, not a control.

Anti-patterns worth naming

  • Flags as long-term config. If the value never varies per user and never changes at runtime, it is a config value. Put it in config.
  • The god-flag. One flag gating a whole release across fifteen files and four services. Turning it off reverts unrelated work, turning it on is a big bang with extra steps. Split it per feature.
  • Flags with no owner. The root cause of nearly every 400-flag codebase.
  • Flipping flags directly in the production database. No audit trail, no propagation guarantee, no review. When something breaks, nobody can say who changed what, or when.
  • Deleting the OFF path but keeping the flag. The quiet one. Old code gets cleaned up, the conditional stays, and flipping the flag off now does nothing (or throws). Your kill switch has become a lie and you find out mid-incident. If the OFF path is gone, the flag goes with it.
  • The zombie experiment. A concluded test still splitting traffic six months later, costing conversions on the losing variant. Every A/B test needs an end date and a cleanup ticket attached at launch.

Start with the taxonomy

Classify before you clean. Give every flag a kind, a name that broadcasts that kind, an owner, and an expiry, and "what is safe to delete" answers itself. Then delete release toggles like laundry, weekly and without ceremony, and leave the ops and permission toggles alone on purpose.

Toggled is a feature flags platform built around this model: owners, kinds and expiry dates as first-class fields, stale-flag reporting, an audit log, and feature toggle management that shows which flags are debt and which are infrastructure. It is in early access with flat per-team pricing. If you are still comparing options, we wrote an honest roundup of the best feature flag tools, and how it works walks through the flow end to end.

Early access · launching soon

Ship your next release behind a flag

Join the early-access list and be first in line when spots open. Flat pricing, unlimited seats, no card required.

No card required · your data stays yours