DevOpsil
Git
93%
Needs Review

Git Bisect: Find the Exact Commit That Broke Production

Sarah ChenSarah Chen7 min read

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

CommandWhat it does
git bisect startBegin 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 resetExit bisect, restore HEAD
git bisect logShow bisect history
git bisect run <script>Automate the session
Script exit codeMeaning
0Good (bug not present)
1124, 126127Bad (bug present)
125Skip 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:

  1. Identify a known-good state (last release tag, last green CI run)
  2. Write a one-liner test that reliably reproduces the bug
  3. git bisect start && git bisect bad HEAD && git bisect good <tag>
  4. If your test is scriptable, run git bisect run and 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.

Share:

Was this article helpful?

Sarah Chen
Sarah Chen

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

GitQuick RefBeginnerNeeds Review

Git Commands: Cheat Sheet

Git commands cheat sheet for DevOps engineers — branching, rebasing, stashing, bisecting, cherry-picking, and recovery workflows with examples.

Sarah Chen·
3 min read

Discussion