The Bug That Only Happens on Their Machine

I once spent three days chasing a race condition in a legacy payment gateway that only triggered when a specific database shard hit its peak I/O limit at exactly 3:00 AM. I remember the specific, hollow feeling in my stomach as I watched the logs scroll by, perfectly clean, while the production metrics screamed that something was fundamentally wrong. Most tutorials will tell you to add more logging or “isolate the environment,” but they never tell you what to do when the environment itself is the liar. If you are currently staring at a screen wondering how to debug a problem you cannot reproduce, you aren’t failing at engineering; you’re just finally dealing with the reality of distributed systems.

I’m not here to give you a checklist of “best practices” that only work in a pristine staging environment. Instead, I want to talk about the messy, unscripted tactics I’ve used over twenty years of maintaining brittle code. I will show you how to build better telemetry, how to use statistical inference when the logs are silent, and how to stop chasing ghosts by changing your fundamental assumptions about the system.

Table of Contents

Hunting Race Conditions and Concurrency Bugs

Hunting Race Conditions and Concurrency Bugs.

Most tutorials treat concurrency like a predictable mathematical equation. They show you a clean semaphore or a mutex and tell you everything will be fine. But after twenty years of maintaining legacy payment gateways, I know that reality is much messier. You aren’t usually fighting a logic error; you’re fighting timing. You’re hunting race conditions and concurrency bugs that only trigger when a specific database shard is under heavy load or when a network packet takes exactly three milliseconds too long to arrive.

When the error is non-deterministic, standard step-through debugging is often useless—the very act of attaching a debugger changes the timing and makes the bug vanish. Instead, I rely heavily on observability and telemetry in debugging. I stop trying to “catch” the error in real-time and start building a better trail of breadcrumbs. I need high-fidelity logs that capture thread IDs and precise timestamps, because if I can’t see the exact interleaving of events that led to the collision, I’m just guessing in the dark.

Isolating Environmental Variables When Everything Looks Fine

Isolating Environmental Variables When Everything Looks Fine

When the code works perfectly on your machine but fails in staging, you aren’t looking for a logic error; you are looking for a difference in the world around the code. I’ve spent enough time on legacy payment gateways to know that the “environment” isn’t just a config file. It’s the subtle difference in kernel versions, the slight latency in a specific subnet, or a sidecar container that’s eating more memory than the documentation promised. Isolating environmental variables becomes a process of elimination where you stop looking at what the code is doing and start looking at what the infrastructure is doing to the code.

If you can’t replicate the failure locally, stop trying to force the same environment. Instead, lean into observability and telemetry in debugging. If the logs are silent, you need more granular metrics—not just “is the service up,” but “what was the exact millisecond delta between this database handshake and the timeout?” You have to treat the production environment like a crime scene. You aren’t there to fix the code yet; you are there to collect the traces of the ghost before it vanishes back into the noise.

Five ways to stop chasing ghosts

  • Stop trying to make the bug happen and start making the environment stay the same. If you can’t reproduce the error, you aren’t debugging; you’re just guessing. I’ve learned that success usually comes from narrowing the delta between your local machine and the production environment until there’s nowhere left for the discrepancy to hide.
  • Instrument the code you thought was “solved.” Tutorials tell you to add a print statement, but in a distributed system, that’s useless. You need structured logging that captures the state of the entire request payload and the specific database transaction ID. When the ghost appears again, you don’t want to guess what the variables were; you want a snapshot of the crime scene.
  • Treat your timestamps like they are sacred. I’ve spent entire weekends realizing a “non-reproducible” bug was actually just a clock drift between two microservices. If your logs don’t have millisecond precision and a unified timezone, you aren’t looking at a timeline; you’re looking at a pile of unrelated events.
  • Look for the “silent killers” in your dependencies. Sometimes the bug isn’t in your logic, but in a library that handles connection pooling or retry logic. If a third-party SDK swallows an exception and returns a null instead of failing, your code will behave unpredictably, and your debugger will show you a perfectly valid, albeit useless, state.
  • Write the failure case before you write the fix. Before I touch a single line of the actual logic, I write a test case that I expect to fail—even if I have to simulate the exact network latency or database lock that caused the original issue. If the test doesn’t fail, you haven’t actually understood the bug yet.

The Hard Truths

Stop trying to replicate the exact moment of failure and start building better telemetry; if you can’t see the state of the system at the millisecond of impact, you’re just guessing in the dark.

When a bug is environmental, the “fix” isn’t usually in the code—it’s in the delta between your local config and the production mess you’re actually running on.

If you find yourself chasing a ghost, stop looking at the logic and start looking at the timing; most “unreproducible” errors are just two processes trying to occupy the same space at once.

The Illusion of the Clean Log

A passing test isn’t proof that your code is correct; it’s just proof that the specific conditions required to break it didn’t happen to be present during your five-minute window of observation.

Pernille Askeland

The Long Game

Debugging complex software issues: The Long Game.

At the end of the day, chasing a non-reproducible bug isn’t about finding a magic command or a clever script; it’s about the slow, methodical process of narrowing the field of possibility. Whether you are hunting down a race condition that only triggers under heavy load or untangling a configuration mismatch that exists only in a specific staging environment, the goal remains the same: eliminate the variables until the ghost has nowhere left to hide. You have to stop trusting your eyes and start trusting the telemetry. If the logs say everything is fine but the system is failing, then your logs are lying to you, and that is exactly where the real work begins.

There is a certain quiet discipline required for this kind of work. It isn’t the adrenaline-pumping excitement of building a new feature from scratch; it is the patience of a bookbinder, carefully aligning edges that refuse to stay straight. You will spend hours, perhaps days, staring at nothing, feeling like you are losing your grip on the logic. But when the pattern finally emerges—when that one outlier in the database or that specific millisecond of latency finally clicks—you realize that you haven’t just fixed a bug. You have truly understood the system, flaws and all. That is the difference between a coder and an engineer.

Frequently Asked Questions

How do I know if I'm actually looking for a race condition or if I'm just chasing a phantom caused by a flaky test suite?

It’s a fine line. If the failure is tied to a specific sequence of events—like a database write hitting a read before the transaction commits—you’re in race condition territory. But if the error feels random, like it’s triggered by a network hiccup or a slow container startup, it’s probably just a flaky test. I usually look for patterns in the timing. If the “phantom” disappears when I throttle the CPU, it’s a race.

When I finally manage to replicate the error in a local environment, how do I ensure I haven't just "fixed" it by changing a variable that wasn't the actual root cause?

The temptation to just tweak a config value and call it a day is a trap. I’ve seen it too many times: you “fix” the bug by increasing a timeout, only for the system to fail again under actual load. To be sure, you have to prove the fix is structural, not cosmetic. Once you replicate it, write a regression test that fails specifically because of the original flaw. If the test passes with your change, you’re on the right track.

At what point do I stop trying to reproduce the bug and just start adding more aggressive telemetry to the production environment?

You stop when your mental model of the system has diverged from reality. If you’ve spent three days tracing execution paths and still can’t find the divergence, stop guessing. You’re no longer debugging; you’re just theorizing. At that point, stop trying to recreate the ghost and start building a better cage. Deploy the telemetry. I’d rather sift through a mountain of high-cardinality logs than spend another week chasing a phantom in a local environment.

About Pernille Askeland

I write the part the tutorial skipped: what it looks like when it breaks.

Scroll to Top