All posts

Zombie Feature Flags: How to Find and Delete Them Before They Find You

·5 min read·
feature flagstechnical debtengineering leadership

Ask any engineering leader how many feature flags their team has, and you'll get a number. Ask where that number came from, and you'll get the count from their flag management dashboard. Then go read the codebase, and you'll find twice as many. The gap between those two figures is where zombie feature flags live, and it's the single most misunderstood category of feature toggle technical debt I know of.

A zombie flag isn't just a stale flag. A stale flag is one that reached 100% rollout a year ago and nobody deleted — annoying, but legible. You know it exists, you know what it does, you can see it in your tooling. A zombie flag is worse: it's a conditional that's still being evaluated in production, still branching your control flow, but has been decoupled from any intentional decision. Nobody owns it. Nobody remembers why it's there. It might be hardcoded to true, read from an environment variable set in a Helm chart three years ago, or gated on a config value no running process ever flips. It's dead in the sense that no human is steering it, and alive in the sense that it still runs on every request. That's the zombie: load-bearing and unaccountable at the same time.

Your flag count is a lie, and that's the real problem

The reason zombie flags are hard isn't deletion. Deleting a flag is a mechanical refactor — unwrap the conditional, inline the surviving branch, delete the dead one. Any competent engineer can do it in an afternoon, and a decent agent can open the PR for you. The hard 90% is the census. You cannot safely delete flags you cannot see, and the flags most worth deleting are precisely the ones that escaped your flag management system in the first place.

Here's why the escape happens. Teams adopt a flag platform and diligently register new flags through it. But flags predate platforms. The if (process.env.NEW_BILLING) check someone added during a hot incident in 2023 never went through the system. The hardcoded const USE_LEGACY_PARSER = false that was supposed to be temporary is a feature toggle in everything but name. The percentage-rollout logic a previous team hand-rolled before you bought a vendor is still evaluating on every request, reading from a database column nobody queries anymore. None of these show up in the dashboard. All of them are zombies. When you tell the board you have 40 feature flags, you mean 40 managed flags. The codebase has 90, and the 50 you can't see are the ones carrying the risk.

How to actually find them

A real inventory of zombie feature flags requires triangulating three sources that most teams never cross-reference:

The code. Static analysis of the repository — not just calls to your flag SDK, but every boolean conditional that reads from configuration, environment, or a magic constant that gates a code path. This is tedious and noisy, which is exactly why it rarely gets done by hand. It's also the only way to find the flags that never touched your platform. Grep for your SDK and you find the managed flags; you have to go looking for the pattern, not the library, to find the rest.

The telemetry. For every candidate flag, you need the evaluation history: how often is it checked, and what does it return? A flag that has returned true on 100% of its hundreds of millions of evaluations over the last year is a zombie — it's doing work on every request to produce an answer that never varies. But — and this is the trap — a flag that has returned true 99.98% of the time is not obviously dead. That 0.02% might be a cron job, a specific enterprise tenant, or a kill-switch that fires only during incidents. The distribution matters more than the headline percentage, and you can't get it from reading code.

The ownership graph. Who created the flag, who last touched the code around it, and are they still at the company? An orphaned flag — one whose owner left and whose team dissolved in a reorg — is a different risk profile than one a current engineer is actively ramping. Orphaned flags are where zombies breed, because the institutional memory that would tell you "oh, don't touch that, it's load-bearing" has walked out the door.

Only when you overlay all three do you get a defensible answer to the question that actually matters: is this flag finished, or is it merely quiet? A flag can be quiet because it's done its job and can be deleted, or quiet because the feature it guards is broken and nobody noticed. Those look identical in a dashboard and opposite in production. Telling them apart is judgment, and judgment doesn't scale when you're staring at 90 candidates.

Why cleanup campaigns fail

Every mature engineering org eventually declares a flag cleanup initiative. A senior engineer gets annoyed, makes a spreadsheet, and the team spends a sprint deleting toggles. It feels great. Then it never happens again, because the work was manual, the reward was invisible, and the risk of touching working code was non-zero the entire time. Eighteen months later the zombie population is back to where it started, plus interest.

This is the core insight about feature toggle technical debt: it's not a cleanup problem, it's a flow problem. One-time campaigns are hopeless because flags accumulate continuously and census-taking is expensive. If finding and deleting zombie feature flags requires a human to periodically go on an archaeological dig through the codebase, telemetry, and org chart, it will not happen often enough to matter. The debt compounds faster than heroic sprints can pay it down. The only durable fix is to make the census continuous and the deletion automatic — to turn a once-a-year archaeology project into a background process that runs every time the code changes.

The shift that actually ends the zombie problem

The deepest version of this fix is upstream of cleanup entirely. Zombies form because flags enter the world unmanaged — created in a hurry, outside the system, with no owner and no lifecycle attached. If every flag is instead born inside the system — registered, owned, and tracked from the first commit that introduces it — the category stops reproducing. You can't have a flag your platform doesn't know about if the platform is the thing that created the flag.

That's the bet DeployRamp makes. Because the flags are inserted automatically when a risky change is detected in a pull request, every flag has provenance, an owner, and an expiry from birth — there's no untracked back door for a zombie to crawl in through. And because evaluation telemetry and code boundaries are watched continuously, "is this flag finished or merely quiet?" gets answered on an ongoing basis rather than during a panicked year-end cleanup. The cleanup agent then opens the removal PR when a flag is genuinely terminal. The zombies you already have still need exhuming, but the ones you haven't created yet simply never become undead. That's the only version of feature flag management that wins, because it stops treating the inventory as something you reconstruct and starts treating it as something you never lose.

Let DeployRamp handle the flags

Install the GitHub App, drop in the SDK, and ship a flagged PR in minutes. Book a demo and we'll show you how.

We use cookies to analyze site usage and improve your experience.