Git Bisect: Find the Exact Commit That Broke Production
Production is down. The symptom is clear — the API is returning 500s on /checkout — but the last deploy included 47 commits from three developers over two days. Which one broke it?
You could check each commit manually. Or you could let git bisect do a binary search through your history and find the culprit in 6 steps instead of 47.
How Binary Search Applies to Git History
git bisect uses binary search. You tell it a "good" commit (known working) and a "bad" commit (known broken). Git checks out the midpoint. You test it, mark it good or bad, and git narrows the range by half each time. For 47 commits, that's at most 6 checks — log2(47) ≈ 5.6.
47 commits → max 6 tests to find the culprit
1000 commits → max 10 tests
1,000,000 commits → max 20 tests
This is the right tool when you know a bug was introduced somewhere in a range of commits and you have a reproducible test for the bug.
Basic Workflow
Step 1: Start bisect
git bisect start
Step 2: Mark the bad commit (current broken state)
git bisect bad # marks HEAD as bad
# or mark a specific commit:
git bisect bad a3f9c12
Step 3: Mark the last known good commit
git bisect good v2.4.1 # a tag works fine
# or a specific commit hash:
git bisect good 8e1d4bc
Git immediately checks out the midpoint commit and tells you how many steps remain:
Bisecting: 23 revisions left to test after this (roughly 5 steps)
[c7a1e9f] feat: add discount code validation to checkout
Step 4: Test, then mark good or bad
Run your test, check the symptom, then:
git bisect good # this commit works fine
# or
git bisect bad # this commit shows the bug
Git checks out the next midpoint. Repeat until bisect reports:
c3d8f21 is the first bad commit
commit c3d8f21
Author: Jordan Lee <[email protected]>
Date: Wed Mar 26 14:33:01 2026 +0000
refactor: extract payment service into separate module
:040000 040000 a1b2... c3d8... M src
Step 5: Clean up
git bisect reset # returns you to HEAD, exits bisect mode
Automated Bisect with a Script
The real power of git bisect is automation. If you can write a script that exits 0 for good and non-zero for bad, git will run the entire bisect without you.
Example: testing an HTTP endpoint
#!/bin/bash
# bisect-test.sh
# Build and start the app
npm run build 2>/dev/null
npm start &
APP_PID=$!
sleep 2
# Test the endpoint
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/checkout)
# Kill the app
kill $APP_PID 2>/dev/null
# Exit 0 = good, 1 = bad (bisect convention)
if [ "$STATUS" = "200" ]; then
exit 0
else
exit 1
fi
Then run it:
git bisect start
git bisect bad HEAD
git bisect good v2.4.1
git bisect run ./bisect-test.sh
Git runs the script at each midpoint automatically and reports the culprit when done.
Example: testing with pytest
#!/bin/bash
# bisect-test.sh
pip install -r requirements.txt -q
python -m pytest tests/test_checkout.py::test_payment_processing -x -q
Exit code from pytest is already 0 on pass and 1 on fail — no extra logic needed.
git bisect run bash bisect-test.sh
Example: testing a compiled binary
#!/bin/bash
make build 2>/dev/null || exit 125 # exit 125 = skip this commit
./bin/myapp --test-mode
exit $?
Exit code 125 is special: it tells git bisect to skip this commit (e.g., it doesn't compile) and move to the next one.
Dealing With Merge Commits and Skip
Sometimes a commit in the range is untestable — it's mid-refactor, the tests don't apply, or it simply doesn't build. Use skip:
git bisect skip # skip current commit
git bisect skip a1b2c3d e4f5a6b # skip multiple commits at once
Bisect will note that it can't determine which commit exactly introduced the bug but will narrow it down to a small range.
Real Scenario: Finding a Performance Regression
Bugs aren't always crashes. Suppose your API's p99 latency went from 120ms to 890ms somewhere in the last 200 commits.
#!/bin/bash
# perf-bisect.sh
npm run build -s
npm start &
APP_PID=$!
sleep 3
# Run 20 requests, capture p99
P99=$(hey -n 20 -c 5 http://localhost:3000/api/search | grep "99%" | awk '{print $2}' | sed 's/secs//')
kill $APP_PID 2>/dev/null
# Fail if p99 > 300ms
if (( $(echo "$P99 > 0.300" | bc -l) )); then
exit 1
fi
exit 0
git bisect start
git bisect bad HEAD
git bisect good v3.1.0
git bisect run ./perf-bisect.sh
This works the same way — bisect will find the commit that pushed latency past 300ms.
Bisect Log and Replay
When debugging as a team, you can save and share bisect state:
# Save the log of your bisect session
git bisect log > bisect.log
# Someone else can replay it
git bisect replay bisect.log
This is useful for handing off a bisect in progress at the end of your shift.
Visualizing the Range
Before you start bisecting, understand what's in the range:
# See all commits between good and bad
git log --oneline v2.4.1..HEAD
# Narrow to specific paths if the bug is isolated to one area
git log --oneline v2.4.1..HEAD -- src/payments/
# Count commits in range
git rev-list --count v2.4.1..HEAD
If the bug is in src/payments/, you can set a bisect filter:
git bisect start HEAD v2.4.1 -- src/payments/
This only tests commits that touch the src/payments/ path, skipping unrelated commits automatically.
Reference: Exit Codes and Commands
| Command | What it does |
|---|---|
git bisect start | Begin a bisect session |
git bisect bad [rev] | Mark commit as broken |
git bisect good [rev] | Mark commit as working |
git bisect skip [rev] | Skip untestable commit |
git bisect reset | Exit bisect, restore HEAD |
git bisect log | Show bisect history |
git bisect run <script> | Automate the session |
| Script exit code | Meaning |
|---|---|
0 | Good (bug not present) |
1–124, 126–127 | Bad (bug present) |
125 | Skip this commit |
128+ | Abort bisect immediately |
When Bisect Won't Help
Bisect works best when:
- The bug is consistently reproducible
- You can define a clear pass/fail test
- You have a known-good commit to anchor the search
It's less useful when the bug is flaky (intermittent), involves external state (database migrations gone wrong), or was introduced by multiple commits that depend on each other. For flaky bugs, run your test script multiple times per commit and use a threshold-based exit decision.
The Workflow That Saves Hours
The next time production breaks after a large batch of commits, resist the urge to manually git checkout your way through history. Instead:
- Identify a known-good state (last release tag, last green CI run)
- Write a one-liner test that reliably reproduces the bug
git bisect start && git bisect bad HEAD && git bisect good <tag>- If your test is scriptable, run
git bisect runand walk away
You will have a specific commit hash, author, and diff to look at in under 10 minutes — regardless of how many commits are in the range.
Was this article helpful?
CI/CD Engineering Lead
Automation evangelist who believes no deployment should require a human. I write pipelines, break pipelines, and write about both. Code-first, always.
Related Articles
Git Commands: Cheat Sheet
Git commands cheat sheet for DevOps engineers — branching, rebasing, stashing, bisecting, cherry-picking, and recovery workflows with examples.
Git Push Rejected: How To Fix "Updates Were Rejected Because The Remote Contains Work You Do Not Have"
You've seen this error. Everyone has. You try to push your changes and Git stops you cold: Here's your fast-track reference to diagnose and fix it. --- The...
Git Worktrees For Managing Multiple Feature Branches Simultaneously
Stop stashing. Stop context-switching. Git worktrees let you check out multiple branches simultaneously in separate directories — each with its own working...
Fix Git 'fatal: refusing to merge unrelated histories'
Resolve the Git error 'refusing to merge unrelated histories' when pulling or merging branches that share no common ancestor.
Pre-Commit Hooks Framework for Automated Code Quality
Set up the pre-commit hooks framework to automatically enforce linting, formatting, and security checks before every Git commit.
DNS Troubleshooting for DevOps: dig, nslookup, and Common Failures
A practical DNS troubleshooting guide for DevOps engineers — dig commands, nslookup patterns, common production failure modes, and how to diagnose each one.