tech, developers, and the code underneath

issue 129· essay·

Feature flags and the state space nobody tests

Twenty flags is a million configurations. You are testing one of them. Here's how to keep that from being a problem.

Feature flags decouple deploy from release, enable gradual rollout, and give you a kill switch. All of that is genuinely valuable.

They also multiply your state space by two for every flag you add, and almost nobody accounts for that.

the arithmetic#

Twenty boolean flags is 2^20 possible configurations — over a million. Your test suite exercises the default configuration. Your staging environment exercises one other. Production is running dozens simultaneously, because different flags are on for different user segments.

Which means: most of the configurations your users are running have never been executed anywhere before.

Most of the time this is fine, because most flags are independent. The failures come from the ones that are not, and you find out about those from a support ticket.

the failure modes#

Interaction bugs. Flag A changes the data format. Flag B reads that data. Both work alone. Together, one of them is reading a shape it does not expect. This is the classic and it is very hard to catch because neither change is wrong.

Flag debt. A flag that has been at 100% for a year is still in the code, with its dead branch, and nobody remembers whether it can be removed. The dead branch does not compile-error and does not test-fail; it just sits there being wrong.

The stale branch. Once a flag is at 100%, the disabled path stops being exercised. Six months later someone flips it as a rollback and discovers the old path no longer works with the current schema. Your kill switch is broken and you find out during an incident.

Configuration drift. Flags set differently in staging and production means staging tests a configuration nobody runs.

the rules that keep this manageable#

Every flag has an owner and an expiry date at creation. Not a suggestion — a required field. A flag that has passed its expiry shows up in a report and somebody has to either extend it with a reason or delete it.

Flags are deleted, not left at 100%. The cleanup is part of the work, not a follow-up ticket. A flag rollout is not done when it reaches 100%; it is done when the flag and the dead branch are gone.

Flags never nest. If flag A only means something when flag B is on, you have created a configuration nobody can reason about. Combine them into one flag with three states, or restructure.

Kill switches are a separate category with separate rules. They live forever, they are documented, and they are tested on a schedule — a quarterly exercise where you flip each one in staging and verify it works. An untested kill switch is not a kill switch.

Test both branches. Your test suite should run the critical path with each significant flag both on and off. Not the full combinatorial space — that is impossible — but each flag independently against the default configuration. That catches the "old path rotted" failure, which is the expensive one.

the taxonomy that helps#

Four kinds of flag, with different lifecycles:

kindlifetimeexample
releasedays to weeksshipping a new checkout
experimentweeksA/B test
ops / kill switchpermanentdisable recommendations
permissionpermanententerprise-tier feature

The first two must expire. The last two must be documented and tested. Conflating them is how you get a thousand flags and no idea which matter.

the observability part#

Every event you log should carry the flag configuration that produced it.

json
{ "event": "checkout", "status": 500, "flags": ["new_pricing", "fast_path"] }

Without this, a bug that only affects one flag combination is undiagnosable — you see errors, you cannot correlate them to anything, and you spend a day guessing. With it, the correlation is a single query.

This is a fifteen-minute change and it is the single highest-value thing you can do if you use flags at all.

the counterargument#

Some teams respond to all of this by using fewer flags and doing more trunk-based deployment with fast rollback.

That is a legitimate position and it works when your rollback is genuinely fast and your changes are genuinely reversible. Flags are a tool for when they are not — when a change is expensive to revert, when you need per-segment control, or when release timing must be decoupled from deployment for business reasons.

Use them for those. Do not use them because they feel safer, because a flag you do not test is not safety, it is the appearance of it.

Dom, March 2, 2026

get README in your inbox

One dispatch, no noise. Tech and developer news, plus the occasional long piece on the craft.

subscribe →