Chapter 07
Cleaning up
Flags come in two kinds. Release flags and experiment flags are temporary: they exist to get a change out safely or to measure it, and once that's done they're debt. Operational flags are permanent on purpose: a kill switch for an expensive feature under load, a disaster recovery switch, or an entitlement that decides which plan gets a feature.
Cleanup is for the first kind.
A temporary flag you leave behind is a code path nobody tests anymore, and it can cost money too. The web SDK evaluates every active flag available to the browser on load, so a request can bill for a flag your code stopped checking months ago.
Let the rollout state decide
For a temporary flag, the rollout state tells you which code path to keep Fig. 1:
| Flag | Rollout | Age | In code | Verdict |
|---|---|---|---|---|
| new-checkout | 100% | 4 mo | 3 files | Remove |
| beta-nav | 0% | 5 mo | 1 file | Remove |
| pro-exports | 40% | 2 mo | 2 files | Ask first |
| Rollout state | Keep | Then |
|---|---|---|
| 100%, with no property filters | The enabled path | Remove the checks, deploy, confirm nothing still checks it, and delete the flag |
| 0% | The fallback | Remove the new code and the checks, deploy, confirm nothing still checks it, and delete the flag |
| Below 100%, or 100% of a filtered group such as a beta cohort | Nothing yet | Ask the owner whether the rollout stalled, is still running, or is meant to stay targeted. A flag that stays targeted is permanent |
| Permanent | The flag | Tag it keep, ops, or kill-switch, so cleanup skips it |
PostHog calls a flag stale when nothing has checked it in 30 days, or when it has sat at 100% for 30 days with no property filters. A flag you turned off on purpose isn't stale. A kill switch sits at 100% for months by design, so it can look stale, which is what the tag is for.
Remove the code first, then the flag
Remove the checks from your code, deploy, and only then delete the flag. For a flag at 100%, the deploy keeps the enabled path, so nobody loses the feature. In the other order, every leftover check loses its answer and falls back to your default, and the feature vanishes for everyone.
Before you delete, confirm nothing still checks the flag. A deleted flag returns false or undefined, so an old mobile app version or a tab left open flips to the off path. The $feature_flag_called events stopping is a signal, not proof: SDKs send the event for locally evaluated and bootstrapped flags too, but not for checks with event capture turned off, such as send_event: false in the browser or sendFeatureFlagEvents: false in Node, and not for a Node getFeatureFlagPayload() call. Search your code for the key, and wait for old app versions to age out.
Make cleanup a process
Most flag debt comes from flags nobody owns. When you create a flag, give it an owner, a name that says what it's for, and a note on whether it's temporary or permanent. The owner of a temporary flag also owns its removal.
Then let a process do the remembering. A scout can check your flags every day and report the ones that are done. When the cleanup is an obvious code change, it marks the report actionable, and a coding agent opens a draft pull request for you to review. It leaves alone any flag whose key it can't find in your code, and any flag tagged keep, ops, or kill-switch, so nothing disappears that it can't trace.











