- Git bisect uses binary search to find the first bad commit in O(log n) time instead of O(n) manual checking.
- The workflow is five commands: start bisect, mark bad commit, mark good commit, test and mark each checkout, reset when done.
- Automate bisect with 'git bisect run <test-script>' for reproducible tests that exit 0 for pass and 1 for fail.
- Common pitfalls include forgetting to reset (leaves detached HEAD), marking commits incorrectly, and using flaky tests that give inconsistent results.
When 47 Commits Stand Between You and Sanity
You run your test suite. 12 failures. The deploy worked fine last week. Somewhere in the last 47 commits, someone broke the authentication flow, and now you’re staring at git log --oneline wondering if you should just git blame every file and start a fight.
There’s a faster way. git bisect does binary search through your commit history. Instead of checking all 47 commits, you check 6. It’s logarithmic (), and you don’t need to understand the math to use it.
Here’s the thing: most tutorials explain bisect like it’s some advanced Git wizardry. It’s not. It’s five commands, and the workflow is more mechanical than clever. You tell Git which commit is broken, which one was fine, Git picks a middle commit, you test it, repeat. That’s it.

The 5-Command Workflow
Start bisect, mark the bad commit (usually HEAD), mark a known-good commit, test the commit Git checks out, repeat until Git finds the culprit.
# 1. Start bisect
git bisect start
# 2. Mark current commit as bad (where the bug exists)
git bisect bad
# 3. Mark a known-good commit (e.g., last week's deploy tag)
git bisect good v1.2.0
# Git now checks out a commit halfway between good and bad
# You test it (run your test, try the feature, etc.)
# 4. Mark this commit based on your test
git bisect bad # if the bug is present
# OR
git bisect good # if the bug is NOT present
# Git checks out another commit
# Repeat step 4 until...
# 5. Git identifies the first bad commit
# When done, reset to your original branch
git bisect reset
No flags to memorize. No rebase conflicts. Just a conversation with Git: “Is this one broken?” Yes or no.
Real Example: When JWT Validation Started Failing
I had a Flask API where /api/user suddenly returned 401 for valid tokens. The error: jwt.exceptions.InvalidSignatureError. Last successful deploy was 3 days ago (commit a4f3c8b), current HEAD is d9e21fa. That’s 23 commits.
Manual checking would mean:
1. Pick a commit
2. git checkout <hash>
3. Restart the dev server
4. curl the endpoint with a test token
5. Repeat 23 times in the worst case
With bisect, it’s 5 iterations max ().
$ git bisect start
$ git bisect bad d9e21fa # current HEAD (broken)
$ git bisect good a4f3c8b # last known working deploy
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[c7b4e19] refactor: move JWT secret to env config
Git checked out c7b4e19, the middle commit. I tested:
$ python app.py &
$ curl -H "Authorization: Bearer <token>" http://localhost:5000/api/user
{"error": "Invalid signature"}
Still broken. So:
$ git bisect bad
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[e8a93d2] feat: add refresh token endpoint
Tested again. This time it worked — valid JSON response, no 401. So:
$ git bisect good
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[f1c6a47] fix: update PyJWT to 2.8.0
Broken. git bisect bad. One more round:
Bisecting: 0 revisions left to test after this (roughly 1 step)
[b9d4f82] chore: update dependencies via pip-compile
Working. git bisect good. Final result:
f1c6a47c is the first bad commit
commit f1c6a47c
Author: coworker <email>
Date: Mon Mar 10 14:22:33 2026
fix: update PyJWT to 2.8.0
Five test iterations. Found it. The PyJWT 2.8.0 update changed the default algorithm behavior — it now requires explicit algorithms=["HS256"] in jwt.decode(). The “fix” commit broke production because someone didn’t test it against real tokens.
Why Binary Search Beats Linear Blame
If you have commits between good and bad, worst-case comparisons:
- Linear search (checking commits one by one):
- Binary search (bisect):
For 50 commits:
– Linear: up to 50 tests
– Bisect: 6 tests ()
For 200 commits (a month of active development):
– Linear: 200 tests
– Bisect: 8 tests
The gap widens fast. And unlike git blame, bisect doesn’t care about who wrote the code or which file changed. It just tests commits until it finds the boundary between working and broken.
Automating Bisect with a Test Script
If you have a reproducible test (unit test, integration test, script that exits 0 for pass and 1 for fail), you can automate the entire bisect:
git bisect start HEAD v1.2.0
git bisect run pytest tests/test_auth.py::test_jwt_validation
git bisect run <command> runs your command at each step. If the command exits with code 0, Git marks that commit as good. Non-zero exit code → bad. Git repeats until it finds the first bad commit.
Here’s a realistic test script for the JWT example:
# test_jwt_bisect.py
import sys
import jwt
from app import SECRET_KEY # assuming this is in your Flask app config
def test_jwt_decode():
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
print(f"✓ JWT valid: {payload}")
return 0
except jwt.InvalidSignatureError:
print("✗ Invalid signature")
return 1
except Exception as e:
print(f"✗ Unexpected error: {e}")
return 1 # treat unknown errors as bad
if __name__ == "__main__":
sys.exit(test_jwt_decode())
Then:
git bisect start HEAD a4f3c8b
git bisect run python test_jwt_bisect.py
Git will check out commits, run your script, mark good/bad automatically, and finish in under a minute. No manual intervention.
One gotcha: your test script needs to be in the repo at the good commit, or Git will fail when it checks out old commits that don’t have the script. Workaround: keep the test script outside the repo, or make sure it was committed before the bug appeared.
When Bisect Gets Messy: Skipping Commits
Sometimes a commit in the middle is untestable — maybe the build is broken, dependencies are missing, or a migration script didn’t run. You can skip it:
git bisect skip
Git will try a nearby commit instead. If too many commits are skippable (e.g., a broken CI period), bisect becomes less effective. In that case, you might need to manually narrow the range first:
git bisect start HEAD <some-commit-closer-to-bad>
I’m not entirely sure how Git’s skip algorithm works under the hood — the docs say it “tries to find a commit close to the one skipped,” but in practice, if you skip more than 2-3 commits, the bisect path gets weird and might not converge efficiently. My best guess is it falls back to a heuristic rather than strict binary search.

Bisect on a Feature Branch (Not Just main)
You don’t need to bisect from a deploy tag. You can bisect within a feature branch:
git checkout feature/new-auth
git bisect start HEAD feature/new-auth~20 # 20 commits back on this branch
Or between two arbitrary commits:
git bisect start abc123 def456
This is useful when you know the bug was introduced in a PR, but the PR has 30 commits and you need to find which one broke it before merging.
Debugging at 2am?
Dark Chocolate Espresso Beans kept me functional through more late-night bisects than I’d like to admit. The caffeine-to-frustration ratio is unbeatable.
Bisect + Git Reflog: When You Don’t Know the Good Commit
Sometimes you don’t have a tagged release or a known-good commit hash. You just remember “it worked yesterday.” git reflog shows your local history of HEAD movements:
$ git reflog
d9e21fa (HEAD -> main) HEAD@{0}: commit: add user profile endpoint
c7b4e19 HEAD@{1}: commit: refactor JWT config
e8a93d2 HEAD@{2}: commit: add refresh token
f1c6a47 HEAD@{3}: commit: update PyJWT
b9d4f82 HEAD@{4}: commit: update dependencies
a4f3c8b HEAD@{5}: checkout: moving from feature/auth to main
...
If you know the feature worked when you last checked out main (yesterday), use HEAD@{5}:
git bisect start HEAD HEAD@{5}
Reflog is local-only (not pushed to remote), so this only works on your machine. But it’s handy when you don’t have proper tags.
Common Mistakes I’ve Seen
Forgetting to git bisect reset after finishing. Bisect leaves you in detached HEAD state. If you start making commits without resetting, you’ll create orphaned commits that are annoying to recover.
Marking a commit wrong (good when it’s bad, or vice versa). Git doesn’t validate your answer. If you fat-finger git bisect good on a broken commit, bisect will give you the wrong result. If you catch it mid-bisect, you can restart: git bisect reset, then git bisect start again.
Using bisect on uncommitted changes. Bisect checks out different commits, which will fail if you have uncommitted changes. Stash first: git stash, run bisect, then git stash pop after git bisect reset.
Testing the wrong thing. If your test is flaky (passes sometimes, fails sometimes), bisect becomes useless. Make sure your test is deterministic. I once wasted 20 minutes bisecting a race condition that only failed 30% of the time — bisect kept finding different “bad” commits.
When to Use Bisect vs. When to Just Read the Diff
Bisect shines when:
– The commit range is large (>10 commits)
– You have a reliable test (automated or manual)
– The bug is a regression (it worked before, doesn’t work now)
Skip bisect if:
– Only 2-3 commits to check (faster to just git show each one)
– The bug is new behavior, not a regression (nothing to bisect against)
– The codebase is unstable and many commits are unbuildable
If the diff between good and bad is small (say, 5 files changed), just read the diff:
git diff a4f3c8b..d9e21fa
Bisect is for when the diff is overwhelming and you need to narrow it down.
FAQ
Q: Can I use bisect if the bad commit is not on the current branch?
Yes. git bisect works across branches. You can start with git bisect start <bad-commit> <good-commit> where the commits are on different branches. Git will check out commits in the ancestry path between them, regardless of branch names.
Q: What if the test I’m using wasn’t present in older commits?
Keep your test script outside the repo (in /tmp or your home directory), or write it to be backward-compatible. If the test file doesn’t exist in old commits, git bisect run will fail. For manual bisect, you just run your external test at each step — doesn’t matter if it’s in the repo or not.
Q: How does Git know which commit is “first bad” if multiple commits are bad?
Git finds the first commit in the ancestry where the transition from good to bad happens. If commits B, C, D are all bad and A is good, Git reports B as the first bad commit. If you later realize B didn’t introduce the bug (maybe it was there earlier but masked), you can restart bisect with a different good commit further back.
What I Still Don’t Know
I haven’t tested bisect on repos with merge commits and complex branch topologies. The Git docs claim it handles merges fine, but I’m curious if it ever gets confused when the same bug exists on multiple branches that later merge. My guess is it would bisect the first-parent history by default, but I’d need to verify with a real case.
Also, bisect assumes a single point of regression. If two independent commits both broke different things, bisect will find one of them, but not both. You’d have to run bisect again after fixing the first one.
Pick Bisect for Big Ranges, Diffs for Small Ones
If you’re staring at more than 10 commits and a reproducible failure, reach for git bisect. Five commands, logarithmic search, done in minutes. For small commit ranges or exploratory debugging, just read the diffs.
Next time I’m curious to try bisect with performance regressions — marking commits “good” or “bad” based on benchmark thresholds (e.g., git bisect run with a script that exits 1 if latency > 200ms). The docs suggest it works, but I haven’t needed it yet. If you’ve done this, let me know how reliable it was.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,793 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (947 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (759 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (652 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (549 views)