It was 3:14 AM, the kind of hour where the blue light of your monitor starts to feel like a physical weight on your eyes, and I was staring at a failing integration test that hadn’t existed yesterday. I didn’t need a lecture on version control theory or a deep dive into the mathematical elegance of binary search; I just needed to know which specific line of code had turned our payment gateway into a paperweight. Most tutorials on how to use git bisect spend way too much time treating it like a classroom exercise, but when you’re in the middle of a production outage, you don’t want a lecture—you want a scalpel.
I’m not going to waste your time with academic fluff or “best practices” that only work in a perfect world. I’ve spent fifteen years breaking things in production, and I’ve learned that the most valuable thing you can have is a repeatable way to find the needle in the haystack. In this post, I’ll show you the practical, no-nonsense workflow I use to isolate regressions quickly. We’ll skip the hype and focus on the actual commands and the mental model you need to stop guessing and start fixing.
Table of Contents
The Brutal Reality of Finding Broken Commits

The brutal reality of finding broken commits isn’t some elegant, mathematical puzzle; it’s usually a frantic attempt to isolate a single line of code while a production dashboard is bleeding red. You know the feeling. You’ve got a regression in the codebase, but the history is a mess of hundreds of commits, and nobody—not even the person who wrote the code—remembers exactly when the logic started to drift. You could spend hours manually checking out different versions, but that’s a recipe for burnout and human error.
This is where the logic of binary search debugging git actually saves your sanity. Instead of checking every single commit in a linear line, you treat the history like a tree. You pick a point you know is broken and a point you know is stable, and you start cutting the search area in half. It’s about identifying the exact moment the behavior changed.
The catch is that this process is only as good as your ability to test. If your test suite is flaky or takes forty minutes to run, you’re going to hate this. I’ve spent many nights manually running `git bisect bad vs good` markers, praying my manual test was accurate. If you can’t automate the check, you’re just playing a very stressful game of “hot or cold” with your deployment pipeline.
Why Your Tests Failed and Git Bisect Bad vs Good Matters

The problem isn’t just finding the commit; it’s the quality of the signal you’re getting while you’re looking. I’ve spent far too many hours in a state of mounting frustration because I was identifying a regression in the codebase using a test suite that was already flaky. If your test fails because of a race condition or a stale environment variable rather than the actual bug, `git bisect` becomes a liar. You end up marking a “good” commit as “bad,” and suddenly you’re hunting for a ghost in a version of the code that was never actually broken.
This is where the distinction between git bisect bad vs good becomes a matter of professional survival. If you don’t have a deterministic way to prove a commit is broken, you aren’t debugging; you’re just guessing. When I’m doing this manually, I have to be incredibly disciplined about the state of my local environment. If I can’t trust my reproduction steps, I shouldn’t be running the bisect yet.
Once you actually have a reliable, repeatable test, you can move past the manual slog. This is when you can start thinking about a git bisect automate script to do the heavy lifting. But don’t rush into automation until you’ve proven to yourself that your test can fail for only the reason you intend. Otherwise, you’re just automating the process of being wrong.
How to actually survive a bisect session
- Don’t try to be a hero with manual testing. If you can’t script the failure, you’re going to make a mistake. I’ve spent too many hours manually clicking through a UI only to realize I marked a ‘bad’ commit as ‘good’ because I forgot to refresh the cache. If it’s possible, write a tiny shell script that returns 0 or 1 and let `git bisect run` do the heavy lifting while you go grab a coffee.
- Beware the “flaky test” trap. There is nothing more soul-crushing than a bisect that tells you a commit is bad, only for you to run the exact same test again and have it pass. If you hit a flaky test, don’t just keep going. Use `git bisect skip`. It’s better to narrow the window slightly than to follow a false trail into a rabbit hole that leads nowhere.
- Always verify your starting and ending points before you commit to the process. It sounds obvious, but I’ve seen people start a bisect from a tag that they thought was stable, only to realize halfway through that the “known good” version was already broken. Check your baseline. If the baseline is lying to you, the whole bisect is a waste of time.
- Use `git bisect visualize` if you have a decent GUI tool installed. Sometimes seeing the commit graph while you’re halfway through the search helps you realize that the breakage isn’t in the code you’re looking at, but in a dependency change that happened three weeks ago. It gives you a bit of spatial awareness that the command line lacks.
- When you finally find the culprit, don’t just fix the code and move on. This is the most important part. Look at the commit message. Was it a logic error, or was it a change in environment? If it was a change in how we handle database connections, that’s a signal that our other services might be sitting on a similar time bomb. Find the commit, fix the bug, and then document the pattern.
The Post-Mortem Summary
Stop guessing. When a regression hits, don’t spend three hours poking at files and hoping you find the culprit; use bisect to let the binary search do the heavy lifting while you keep your sanity.
Your bisect is only as reliable as your test suite. If your “good” or “bad” markers are based on a flaky test or a manual check you forgot to perform correctly, you’re just searching for a ghost.
The goal isn’t just to find the commit, it’s to find the pattern. Once bisect points you to the broken code, look at the context—the “why” is usually more important than the “where” if you want to stop the next 2 AM page.
The Post-Mortem Mindset

At the end of the day, git bisect isn’t just a utility; it’s a way to stop the bleeding without losing your mind. You’ve learned that the tool is only as good as your ability to define what “broken” actually looks like. If your test suite is flaky or your manual verification is lazy, you’re just performing a mathematical dance toward the wrong conclusion. Remember to automate the check whenever possible, keep your test cases isolated, and—most importantly—be honest about whether a commit is truly bad or if you just had a bad run of luck with a transient network error. It’s about narrowing the search space until the culprit has nowhere left to hide.
I’ve spent enough late nights staring at terminal outputs to know that finding the bug is only half the battle. Once bisect points its finger at that specific commit, don’t just revert it and go to sleep. Take ten minutes to look at why that change was even made in the first place. Was it a rushed hotfix? A misunderstanding of a dependency? The goal isn’t just to fix the regression, but to update our collective understanding so we don’t trip over the same stone in six months. Write it down, fix the test, and then you can finally get some decent sleep.
Frequently Asked Questions
What do I do if I can't even get the project to build at the "good" commit?
This is where everyone usually gives up and goes back to manual debugging, but don’t. If the “good” commit won’t even build, it’s usually because of external drift—a dependency version bumped in the registry or a stale cache, not the code itself.
Is there a way to automate this with a script so I don't have to manually test every single step?
You can. If you have a reliable test script—even just a shell script that exits with `0` on success and `1` on failure—you can use `git bisect run `.
How do I handle a situation where the bug is intermittent and doesn't show up every time I run the test?
This is where `git bisect` starts to feel like a gamble. If the bug is flaky, you’re just playing Russian roulette with your build status. I’ve learned the hard way: don’t trust a single pass. If I suspect intermittency, I run the test in a loop—say, fifty times—before marking a commit as “good.” It’s slower, sure, but it beats marking a broken commit as “good” and spending the next three days chasing a ghost.
Can I use bisect if the bug is related to a database schema change rather than just the code?
Technically, yes, but it’s a headache. Git bisect assumes a linear progression of code, but if your bug is tied to a schema change, you’re fighting the state of the database, not just the logic. If you don’t have a way to roll back the schema or spin up a fresh container with the exact migration state for every step of the bisect, you’ll just be chasing ghosts. It’s much cleaner to isolate the code change first.
