You deploy a change.
Something that definitely worked last week is now very, very broken.
You know two things:
- ✅ It used to work.
- ❌ It’s broken now.
What you don’t know is which commit introduced the bug.
A lot of teams still handle this by guesswork:
- “Maybe it was that big refactor?”
- “Try reverting the logging change?”
- “What if we go back three days?”
This is slow, stressful, and error-prone. Git already has a tool that’s built for exactly this problem: git bisect.
git bisect does a binary search through your commit history to find the exact commit that introduced a regression. Instead of checking every commit, you mark commits as “good” or “bad” and Git narrows it down in O(log n) steps.
In this article, we’ll walk through:
- When to use
git bisect - The basic manual workflow
- How to automate the search with
git bisect run - Practical tips, patterns, and pitfalls
When to reach for git bisect
Use git bisect when:
- You have a regression: something used to work, now it doesn’t.
- You have a repeatable way to test whether a commit is good or bad (a unit test, integration test, curl command, or manual action).
- You can identify:
- A known good commit (the bug wasn’t present)
- A known bad commit (the bug is present)
Typical scenarios:
- A deployment last night broke a feature, but your last known-good tag was from last week.
- A library upgrade sometime in the last 50 commits caused a subtle runtime error.
- A flaky behavior became consistently broken, and you want to find where it got worse.
If you can say “it worked in commit X, and it’s broken in commit Y,” git bisect can almost always help.
How git bisect works (conceptually)
Think of your commit history as a sorted list:
|
1 2 3 |
good -------------------------> bad old commit latest commit |
You tell Git:
- “This commit here is bad.”
- “That older commit over there is good.”
Git will:
- Check out a commit roughly halfway between them.
- You test it and say:
good→ bug not presentbad→ bug present
- Git chooses the next halfway commit between good/bad.
- Repeat until there’s only one possible culprit left.
Instead of eyeballing dozens of commits, you log-walk your way to the bad one in just a handful of steps.
Prerequisites: set yourself up for a good bisect
Before you start:
- Make sure your work tree is clean
12git status
If you have local changes, commit or stash them:
12git stash push -m "pre-bisect work" - Pick a known bad point
Most often this is
HEAD(the current commit):123git log -1# confirm this commit has the bug - Pick a known good point
This can be:
- The last known-good tag (e.g.
v1.2.0) - A specific commit hash
- An older branch point
Confirm that the bug does not occur in that commit.
- The last known-good tag (e.g.
The more confident you are about “good” and “bad,” the more reliable and faster the bisect will be.
Manual git bisect workflow step by step
Let’s walk through a simple manual session.
1. Start the bisect session
From your repo:
|
1 2 |
git bisect start |
2. Mark the bad commit
If the current HEAD has the bug:
|
1 2 |
git bisect bad |
If not, specify a commit:
|
1 2 |
git bisect bad <bad-commit-sha> |
3. Mark a known good commit
|
1 2 3 4 |
git bisect good <good-commit-sha-or-tag> # e.g. git bisect good v1.2.0 |
At this point, Git will check out a commit somewhere between those two.
You’ll see output like:
|
1 2 3 |
Bisecting: 10 revisions left to test after this (roughly 4 steps) [abc1234] Refactor API endpoints |
4. Test and classify each commit
Now, test this commit:
- Run your app
- Hit the endpoint
- Run a test command
- Whatever reproduces the bug
If the bug is present:
|
1 2 |
git bisect bad |
If the bug is not present:
|
1 2 |
git bisect good |
Git checks out the next midpoint commit and tells you how many steps are left. Repeat the test + good/bad marking.
5. When Git finds the culprit
Eventually you’ll get something like:
|
1 2 3 4 5 6 7 8 |
abc1234 is the first bad commit commit abc1234... Author: ... Date: ... Refactor API endpoints ... |
At this point, you’ve found the first commit where the bug appears.
6. Reset git bisect
When you’re done:
|
1 2 |
git bisect reset |
This returns you to the commit where you started (usually your original HEAD).
Skipping “broken” commits
Sometimes a commit can’t be tested:
- It doesn’t compile.
- A migration is half-finished.
- Dependencies are broken for that snapshot.
Instead of forcing “good” or “bad,” you can mark it as skipped:
|
1 2 |
git bisect skip |
Git will pick a different commit. If too many commits in a row are skipped, Git might only narrow it down to a range instead of an exact commit, but that’s still much better than guesswork.
Automating the search with git bisect run
Manual bisecting is already powerful, but it gets even better when you automate the “good or bad?” decision.
If you can express your test as a script or command that:
- exits with
0→ good - exits with non-zero → bad
…then git bisect run can do the entire search for you.
1. Write a test script
Example: your API endpoint /health returns HTTP 500 when broken.
Create a script like test-health.sh:
|
1 2 3 4 5 6 7 8 |
#!/usr/bin/env bash set -e # Build or start the app as needed # For simple cases, maybe just run tests: npm install >/dev/null 2>&1 || exit 125 npm test -- api-health || exit 1 |
A few notes:
- Exit
0if the commit is good (test passes). - Exit non-zero (e.g.,
1) if the commit is bad (test fails). - If you use
exit 125, Git treats that likeskipforgit bisect run.
Make it executable:
|
1 2 |
chmod +x test-health.sh |
2. Start a bisect session as before
|
1 2 3 4 |
git bisect start git bisect bad # or git bisect bad <bad-commit> git bisect good v1.2.0 # or any known-good commit |
3. Let git bisect run do the work
|
1 2 |
git bisect run ./test-health.sh |
Git will:
- Check out a midpoint commit
- Run your script
- Classify as good/bad/skip based on the exit code
- Move to the next commit
- Repeat until it finds the first bad commit
When it finishes, it prints the culprit and leaves you at that commit. Then you can inspect the diff, blame, etc.
Don’t forget to:
|
1 2 |
git bisect reset |
after you’re done.
Practical patterns for git bisect run
Here are some common patterns for the command/script you run with git bisect run:
1. Use your test suite
If the regression is covered by tests:
|
1 2 3 4 |
git bisect run pytest tests/feature/test_login.py::test_happy_path # or git bisect run npm test -- login-feature |
2. Simple shell checks
If the bug is visible via CLI:
|
1 2 |
git bisect run bash -c 'make build && ./myapp --check-something' |
3. HTTP checks with curl
If the bug is “this endpoint returns 500 now”:
|
1 2 3 4 5 6 |
git bisect run bash -c ' docker compose up -d app && sleep 5 && curl -fsS http://localhost:8080/health >/dev/null ' |
You might want to wrap that in a script to clean up containers between runs.
Tips, best practices, and gotchas
Keep the test as fast as possible
git bisect run might run your test many times. A test that takes 60 seconds will make the whole bisect painful.
- Prefer narrow tests over “run the entire suite.”
- Cache dependencies where possible (e.g., use local node_modules, venvs, or build artifacts).
- Avoid full environment rebuilds if you can.
Watch out for non-deterministic tests
Flaky tests confuse git bisect. One run says “bad,” the next says “good” on the same commit.
If you suspect flakiness:
- Add retries or “stabilization” to your test script.
- Tighten the test to something more deterministic.
- As a last resort, use manual
good/baddecisions instead ofrun.
Be mindful of environment changes
If the bug is due to:
- External API changes
- DB schema changes unrelated to code
- Environment differences
…then bisecting across that change might be misleading. You might be bisecting environment drift instead of code changes.
Try to reproduce the bug in an environment where only the code is changing.
Use tags and notes once you’ve found the culprit
Once git bisect identifies the first bad commit:
- Add a note:
12git notes add -m "Introduced regression in login flow" - Reference it in issue trackers.
- Use it as a teaching moment: commit size, review quality, or test coverage issues often show up in bisect results.
Why teams should use git bisect more often
git bisect is one of those tools that feels “advanced” until you’ve used it once or twice. Then it becomes:
- A standard part of your incident response playbook
- A way to reduce blame (“it’s this commit,” not “it’s probably your feature”)
- A nudge toward better tests, because the more behavior you can test automatically, the more powerful bisect becomes
For teams running frequent releases, git bisect pays for itself very quickly in reduced guesswork and faster mean time to resolution.




