Something worked in the release two months ago and does not work now. Between then and now sit four hundred commits. Reading through them looking for the culprit is hours of work with a poor success rate; bisect finds it in roughly nine checks.
The technique is a binary search over history, and the only real skill involved is writing a reliable test.
What changed in 2026
- Automation became the default usage. Running bisect with a script rather than interactively became standard practice, since it is both faster and less error-prone.
- CI-integrated bisect appeared. Triggering an automated bisect from a failing pipeline, rather than reproducing locally, became available in more systems.
- Performance bisecting spread. Using a threshold-based script to find the commit that introduced a slowdown, rather than a functional break, became a common application.
- Agent-driven debugging adopted it. Coding agents began using bisect as a diagnostic step, which requires the same reliable test script a human would write.
The workflow
| Step |
Action |
| 1 |
Identify a commit where the behaviour was correct |
| 2 |
Confirm the current commit is broken |
| 3 |
Start bisect with those two endpoints |
| 4 |
At each step, test and mark good or bad |
| 5 |
Mark unbuildable commits as skipped |
| 6 |
Bisect reports the first bad commit |
| 7 |
Reset to return to your original state |
The manual loop works and is tedious. The better path is writing a script that exits zero when the behaviour is correct and non-zero when it is not, then letting bisect run the whole search unattended. What takes twenty minutes of context-switching becomes a command you walk away from.
The script needs care in one respect: it must be reliable. A test that passes intermittently will mark a good commit as bad, and bisect will confidently report a commit that has nothing to do with the problem. If the failure is flaky, make the script run the check several times and only report success when all runs pass — or fix the flakiness first.
The awkward cases
Commits that do not build. In a long history, some commits are broken for unrelated reasons. Marking them as skipped tells bisect to try a neighbour instead. A script should detect a build failure and signal skip rather than reporting the commit as bad.
Merge-heavy history. Bisect traverses the full commit graph, which on a heavily merged repository means visiting commits from feature branches that were never independently tested. Restricting the search to first-parent history, so it only visits mainline merge points, frequently gives a clearer answer — it identifies the merge that introduced the problem, and you can bisect within that branch afterwards.
Behaviour that requires a rebuild or migration. If the test needs a build step, a database migration, or a dependency install, the script must handle it. This is where most bisect runs go wrong — the script tests a stale artifact and reports nonsense.
The commit is a revert or a merge of an old branch. Bisect finds where the behaviour changed on the current history, which is not always where the code was written. Read the identified commit rather than assuming it is the author's mistake.
Common mistakes
- Bisecting an intermittent failure. Produces a confident wrong answer.
- Not rebuilding in the test script. Tests a stale artifact throughout.
- Marking unbuildable commits as bad. Corrupts the search.
- Forgetting to reset afterwards. Leaves the repository on a detached commit.
- Testing manually for a long range. Automate; the script pays for itself in one run.
- Assuming the identified commit is the whole story. It is where behaviour changed, which may not be where the bug was written.
FAQ
How many steps will it take?
Roughly the base-two logarithm of the commit count. Four hundred commits is about nine checks; four thousand is about twelve.
Can I bisect a performance regression?
Yes. Write a script that measures and compares against a threshold, exiting non-zero when the measurement is worse. Watch for measurement noise, which behaves like flakiness.
What if the good commit is not actually good?
The search will produce a wrong answer. Verify both endpoints before starting rather than assuming.
Does it work with a squashed history?
Yes, and granularity is coarser — you find the squashed pull request rather than the individual commit within it.
Where to go next
For history practices that make bisect work well, read stacked diffs workflow and trunk-based development. For diagnosing what you find, profiling with flame graphs.