Git Bisect Finds the Regression Build Caches Hired a Maintainer to Forget
Git bisect is a binary search over commits, and build caches quietly break the assumption it runs on: that rebuilding a commit reproduces what that commit does. When a cache hit skips the recompile, bisect can land on the wrong commit, or on no commit at all. That failure mode is now common enough that cache hygiene has become part of regression hunting.
The Cache That Ate the Regression
Engineers treat a warm cache as a solved problem. The build is fast, CI is green, and nobody asks why the artifact predates the source change. That is the moment a regression becomes invisible. The cache returns a green result for a commit that would fail if compiled from scratch, and the person debugging trusts it.
Bisection is the standard method for identifying the change set that introduced a behavior change, and it is mostly used to find the patch that introduced a bug. Its whole value depends on each step being a fresh, honest build of the commit under test. Caches insert a shortcut between the commit and the artifact.
Maintainers inherit the debt. A cache configured by a team that has since moved on keeps serving stale objects, and the person who finally hunts the regression has no record of which keys were trusted or when they were last verified. The tool is fast. The diagnosis is slow.
How Bisect Lost Its Power
Bisection assumes deterministic rebuilds: same inputs, same output, every time. Cache hits skip the recompile step entirely, so the artifact bisect tests may correspond to an older tree. The search proceeds on false premises and reports a commit that merely happens to sit near a cache boundary.
The failure is not random. It clusters around commits that touch files the cache key ignores, such as generated headers, environment files, or lockfiles that a hashing scheme does not cover. Those commits look cheap to build and are exactly the ones the cache serves from memory.
Git bisect run automates the loop and reports false positives with total confidence. It exits nonzero on a commit that never actually failed a clean build. The terminal output looks authoritative, which is why teams burn a day before someone suggests clearing the cache and starting over.
The same class of problem shows up in adjacent infrastructure. This site has argued in a related piece on firmware maintenance that rollback windows, not drivers, constrain accelerator upkeep. Caches are a similar constraint layer: the tooling decides what is even visible to test.
A Maintainer's Tuesday in 2026
Start bisect, get a clean result, and feel briefly relieved. Then notice the failing test passes on the commit bisect named. The instinct is to re-run, which returns the same answer, because the cache is still warm and still wrong.
The fix is manual. Clear the cache, rebuild, re-run the single step, and watch it fail. Repeat per commit. A hunt across a few hundred commits can mean roughly 40 to 60 rebuilds once cache invalidation is done by hand, and each one costs minutes on a large monorepo.
Remote caches make this worse. A shared cache means a poisoned artifact can reach every developer and every CI runner, so clearing locally does not clear the problem. The maintainer ends up auditing cache keys while also trying to read a stack trace.
None of this appears in the onboarding doc. The cache is documented as a speed feature, and its invalidation rules are documented nowhere at all.
The Tradeoff Nobody Documented
Caches cut CI times substantially, often by half or more on large builds, and that saving is real. The cost is that correctness now depends on cache key design, a subsystem most teams treat as plumbing rather than as part of the test contract.
Flaky dependencies get masked too. A cache can serve a previously good artifact for a dependency that has since regressed, so the failure surfaces later on a clean machine and looks unrelated to the commit that triggered it. Debugging starts in the wrong place.
Remote caches share poisoned artifacts across trust boundaries. One bad upload can seed every downstream build, and the audit trail is a set of opaque keys. Package registries face a comparable economics problem, as a related piece on publish fees and mirror bandwidth notes, but at least a registry version is immutable and inspectable.
The objection from platform teams is fair: disabling caches for every bisect makes hunts expensive. That is true, and it is still cheaper than a wrong answer. The trade is speed for certainty, and it should be made explicitly, per hunt, not by default.
What Good Cache Hygiene Looks Like
Hash inputs, not timestamps. A cache key built from file mtimes will miss content changes and hit on unrelated edits. Content hashes over every file that affects the output, including generated files and toolchain versions, are the only keys that survive a bisect.
Verify cache keys per commit during a hunt. If the key for the commit under test matches a key already in the cache, treat the result as unverified until a clean build confirms it. Log every cache hit and miss so the hunt has evidence rather than impressions.
Run bisect with the cache disabled as the default for regression work, and re-enable it once the culprit commit is known. The performance loss is bounded by the number of steps in the search, which is logarithmic. The correctness gain is the entire point of the exercise.
Cache replacement policies matter less than key design. A cache with a perfect eviction algorithm and a lazy key will still serve stale artifacts. Fix the key first, then tune eviction.
One more habit: keep a separate cache namespace for bisect runs. If your cache supports tags or namespaces, a dedicated namespace prevents bisect artifacts from polluting the main build cache and makes cleanup a single command. That isolation also gives you a clear audit boundary when you need to prove which artifacts were used during a hunt.
Five Moves for Your Next Hunt
Disable the build cache before starting bisect, and note in the bisect log that caching was off so the result is reproducible by someone else.
Rebuild from a clean checkout at each step, or at minimum clear the specific cache namespace that covers the files touched by the commit range.
Audit the remote cache weekly for artifacts whose source commit no longer exists or whose key cannot be recomputed from the current tree.
Add a bisect smoke test to CI that runs a short, known regression range with caching disabled and asserts that bisect finds the expected commit.
Document the cache invalidation rules next to the cache configuration, including which inputs are hashed and which are deliberately excluded, so the next maintainer does not rediscover them under pressure.
Tooling and Culture
Cache invalidation tools are immature relative to build tools. Most build systems offer a way to clear the cache, but few offer a way to inspect why a particular key was considered valid. That gap forces maintainers to reason about cache behavior from first principles, often under time pressure.
Some teams have started treating cache configuration as code that requires review, the same way they treat CI pipelines. That practice catches key omissions before they cause a bisect failure. It also creates a paper trail when a cache-induced regression does slip through.
Others run periodic cache audits that replay a sample of recent builds with caching disabled and compare outcomes. The approach is not free, but it surfaces drift before it corrupts a hunt. The cost is a fraction of the CI budget, and the payoff is confidence in the cache.
Culture matters as much as tooling. If the team treats cache as an untouchable speed feature, bisect will keep lying. If the team treats cache as part of the test contract, bisect stays honest. The choice is not technical; it is about what the team values more: fast builds or trustworthy results.
The tooling will improve. Build systems are adding better cache introspection and more granular invalidation. Until then, the maintainer's best defense is a healthy skepticism toward any green build that arrived suspiciously fast.