Nobody knows which feature flags are safe to remove
“There are hundreds of flags, many at 100% for a year. Nobody remembers who created them, removing one feels risky, and some code paths are only reachable with combinations nobody tests.”
THE PRINCIPLE
A release flag isn't done when it hits 100%; it's done when the flag and the old path are deleted.
FIRST MOVES
- Classify every flag: release, experiment, operational, kill switch, or permission. Only kill switches and permission flags should be long-lived; release, experiment and most operational flags are temporary.
- Give each flag an owner and an expiry at creation, in one typed registry the code must go through.
- Find removal candidates: flags fully on or fully off for weeks, flags not evaluated recently, and flags your flag service marks stale or potentially stale (Unleash does this automatically once a flag outlives its type's expected lifetime, for example 40 days for release flags).
- Make expiry enforceable: a CI check that fails on overdue temporary flags (warn the owner a week ahead so it doesn't ambush an unrelated PR), or a removal ticket created with the flag.
- Remove in one PR: hard-code the winning value, delete the dead branch and its tests, then archive the flag in the service.
TOOLS: THEN → NOW
Ad-hoc if (config.x) checks and env varsA vendor-neutral evaluation API (OpenFeature) in front of any flag provider
Manual spreadsheet cleanupsFlag types with expected lifetimes and stale markers (for example Unleash), plus CI expiry checks
PATTERN SNAPSHOT
import { OpenFeature, type EvaluationContext } from "@openfeature/server-sdk";
export const flags = {
"checkout-v2": { type: "release", owner: "payments", expires: "2026-12-15", fallback: false },
"pricing-killswitch": { type: "kill-switch", owner: "pricing", expires: null, fallback: true },
} as const;
export const isOn = (key: keyof typeof flags, ctx: EvaluationContext) =>
OpenFeature.getClient().getBooleanValue(key, flags[key].fallback, ctx);
// flags.test.ts: fails CI once a temporary flag outlives its expiry
test("no overdue flags", () => {
const today = new Date().toISOString().slice(0, 10);
const overdue = Object.entries(flags).filter(([, f]) => f.expires !== null && f.expires < today);
expect(overdue).toEqual([]);
});CLOSE THE AI. EXPLAIN THIS.
Which flag types are expected to be long-lived, and what has to happen besides switching a release flag to 100%?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
Guide reviewed