Retiring feature flags after launch Jump to heading
Feature flags are what make trunk-based development work: unfinished features merge daily and stay dark until ready. Every one of those flags is also a branch in the code β two paths, two behaviours, twice the testing β and once the feature is launched, the old path is dead weight. Teams that adopt flags without a retirement habit end up with hundreds of them, some fully on for years, some guarding code nobody remembers, and an occasional outage when someone toggles a flag everyone assumed was obsolete. Retiring flags is ordinary engineering work, but it needs structure: every flag has an owner and an expiry from the day it is created, stale flags are found mechanically, and removal happens in safe steps. This page sets that up, within trunk-based development setup.
When to use this approach Jump to heading
- Your codebase uses feature flags for unfinished or gradually rolled-out work.
- Nobody can say how many flags exist or which ones are still needed.
- Flags that have been fully on for months still have both code paths.
- You are starting to use flags and want to avoid accumulating them, as in feature flags vs feature branches for unfinished work.
Step 1 β Register every flag with an owner and an expiry Jump to heading
Keep a registry of flags in the repository: name, owner, purpose, creation date and expected removal date. Creating a flag means adding an entry in the same pull request.
# flags.yml β every flag the code may read
- name: export-scheduling
owner: "@acme/data-platform"
purpose: Gate the per-account export scheduler during rollout
created: 2026-09-14
remove_by: 2026-11-30
type: release # release | experiment | ops | permission
- name: payments-new-provider
owner: "@acme/billing"
purpose: Branch-by-abstraction switch to the new payment provider
created: 2026-08-02
remove_by: 2026-10-31
type: release A CI check keeps the registry and the code in sync: every flag referenced in code must be registered, and every registered flag must still be referenced or be removed from the registry.
git grep -hoE 'flags\.enabled\("[a-z0-9-]+"' -- 'src/**' | sed 's/.*("//; s/"//' | sort -u > /tmp/flags-used
yq -r '.[].name' flags.yml | sort -u > /tmp/flags-registered
comm -23 /tmp/flags-used /tmp/flags-registered | sed 's/^/unregistered: /'
comm -13 /tmp/flags-used /tmp/flags-registered | sed 's/^/no longer used: /' Step 2 β Find stale flags mechanically Jump to heading
A flag is a removal candidate when its expiry has passed, or when it has been fully on (or off) in every environment for some weeks. List both kinds weekly.
today=$(date +%F)
yq -r '.[] | select(.type=="release" or .type=="experiment") | select(.remove_by < "'"$today"'") |
"\(.remove_by) \(.name) \(.owner)"' flags.yml Git history adds another signal: when did the flagβs code paths last change? A flag whose guarded code has not been touched in months is usually settled.
git log -1 --format='%cs' -S'"export-scheduling"' -- src/ Step 3 β Remove flags in safe steps Jump to heading
Removing a flag is a code change with production consequences: it hard-codes one path. Do it in steps that can each be undone.
- Confirm the flagβs value is the same everywhere and has been for a while.
- Remove the flag check in code, keeping the winning path β one pull request.
- Deploy, and only then remove the flag from the flag service and the registry.
git switch -c chore/remove-flag-export-scheduling origin/main
git grep -n '"export-scheduling"' -- src/ tests/
# edit each site: keep the enabled branch, delete the disabled branch and its tests
git commit -am "chore: remove export-scheduling flag (launched 2026-10-12)" Removing the code before the flag-service entry means a deploy rollback still finds the flag defined and behaves correctly.
β οΈ SAFETY WARNING: Removing a flag check that was not truly at the same value everywhere changes behaviour for some customers β for example, a flag fully on in production but off for one enterprise tenant. Check per-environment and per-segment values in the flag service before removal. If behaviour changed unexpectedly, revert the removal commit; the flag still exists in the service until step 3 completes.
Step 4 β Make retirement part of the launch Jump to heading
The easiest time to remove a flag is right after launch, while its author remembers the code. Add βremove flagβ as a final task in the same ticket that launches the feature, scheduled a couple of weeks after full rollout.
# Open a removal pull request automatically for flags past their date
yq -r '.[] | select(.remove_by < "'"$(date +%F)"'") | .name' flags.yml | while read -r f; do
gh issue create --title "Remove flag $f" --label flag-cleanup \
--body "Flag \`$f\` passed its remove_by date. Owner: $(yq -r ".[] | select(.name==\"$f\") | .owner" flags.yml)"
done Step 5 β Watch the total Jump to heading
Track the number of release and experiment flags over time. A steady or falling count means retirement keeps pace with creation; a rising one means the habit has slipped.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What about flags that should live forever? Jump to heading
Kill switches and permission gates are legitimate long-lived flags. Register them with type ops or permission and no removal date, so they are distinguishable from forgotten release flags.
How do we remove a flag that guards complicated code? Jump to heading
The same way as any refactor: in small steps, with tests on the winning path first. If the code is tangled, removing the flag is often a good moment to simplify it, in a follow-up pull request.
Should tests cover both flag states? Jump to heading
While the flag is live, yes. When the flag is removed, delete the tests for the losing path along with the code.
Related Jump to heading
- Trunk-Based Development Setup β the parent topic.
- Branch by Abstraction for Large Changes β flags used as a switch-over.
- Limiting Feature Branch Lifetime β flags as the enabler of short branches.
- Finding When Code Appeared with the Pickaxe β tracing a flagβs history.