How to spot a race condition while reading.

•

•

Reading Code for Things That Happen at Once

I once spent three days chasing a ghost in a logistics system that only appeared when the server load hit a very specific, unlucky peak. I was staring at lines of code that looked perfectly logical in isolation, yet somehow they were conspiring to corrupt our database. Most tutorials try to teach you how to use heavy-duty formal verification tools or complex static analyzers to find these bugs, but that’s not how it works in the real world. If you want to actually survive a production outage, you need to learn the manual art of how to spot a race condition while reading through a pull request before it ever hits the main branch.

I’m not going to give you a lecture on theoretical concurrency models or throw a bunch of academic jargon at you. Instead, I want to show you the specific, uncomfortable patterns I’ve learned to look for after a decade of seeing things break in ways that shouldn’t be possible. We’ll focus on the practical red flags—the shared state, the unsynchronized reads, and the deceptive simplicity of “just one more line”—that tell me a bug is hiding in plain sight.

Table of Contents

Detecting Data Races in Source Code Before They Strike

Detecting Data Races in Source Code Before They Strike

When I’m sitting there with a pull request open, I don’t look for logic errors first; I look for shared state. Most concurrency bugs identification doesn’t happen because the math is wrong, but because the timing is unpredictable. I’ve learned to scan for any variable that isn’t local to a function or protected by a mutex. If you see a class-level property being modified inside a method that’s likely to be called by a worker pool, your internal alarm should go off. You aren’t just looking for code; you are looking for unprotected access points where two threads might collide.

It’s also worth distinguishing between what you think is a simple increment and what is actually a dangerous operation. I see this constantly: someone uses a standard integer increment in a loop, thinking it’s a single step. In reality, that’s a read, a modify, and a write. If you don’t swap those for atomic operations, you’re essentially inviting a race condition to dinner. I try to treat every non-local mutation as a potential failure point. If the code doesn’t explicitly show how it handles multithreading synchronization issues, I assume it’s broken and wait for the author to prove me wrong.

Identifying Non Deterministic Behavior in Your Logic

Identifying Non Deterministic Behavior in Your Logic

When I’m reviewing a PR and I see a block of code that relies on “the order of things just working out,” my internal alarm goes off. Identifying non-deterministic behavior usually starts with looking for any logic that assumes a specific sequence of events without explicit enforcement. If you see a function that reads a value, performs a calculation, and then writes it back—without a lock or a Compare-and-Swap—you aren’t looking at a feature; you’re looking at a ticking time bomb. This is the essence of concurrency bugs identification: finding the places where the developer assumed the CPU would be more orderly than it actually is.

I’ve spent too many hours debugging systems where the outcome of a function changed every third run because of a slight shift in thread scheduling. To catch this while reading, look for any shared state that isn’t protected by a clear synchronization primitive. If you see a global variable or a shared object being mutated inside a loop, ask yourself: what happens if another thread interrupts this exact line? It’s rarely about whether the code is “correct” in a single-threaded test environment, and almost always about whether your multithreading synchronization issues will manifest when the load hits production and the timing gaps start to close.

My mental checklist for spotting the cracks

  • Look for the “read-modify-write” pattern. If you see a variable being fetched, updated in local memory, and then pushed back, you’ve found a target. If there isn’t an explicit lock or an atomic operation wrapping that entire sequence, someone is going to overwrite a change made by another thread in the millisecond between the read and the write.
  • Watch out for “naked” shared state. I tend to scan for any object or collection that lives outside the immediate scope of a function but is being passed around or accessed by multiple workers. If it’s global, a singleton, or a long-lived class member that isn’t wrapped in a synchronization primitive, it’s a red flag.
  • Check the granularity of your locks. It’s easy to think you’re safe because you used a mutex, but if the lock is released before the dependent operation finishes, or if you’re using different locks for different parts of the same logical transaction, you haven’t actually protected the data—you’ve just added overhead.
  • Hunt for “check-then-act” sequences. This is a classic. You see code that checks `if (list.isEmpty())` and then immediately calls `list.remove()`. It looks perfectly logical on the surface, but in a high-concurrency environment, that list can stop being empty in the tiny window between the check and the action.
  • Question the assumptions about external dependencies. We often assume a database or a cache call is an atomic unit of work, but if your code relies on the state of a remote system to make a local decision, you’re playing a dangerous game of musical chairs. If the state changes mid-flight, your local logic is now based on a lie.

The Hard Lessons

Stop looking for “broken” code and start looking for shared state that isn’t protected by a clear, single source of truth; if two different functions can touch the same variable without a visible gatekeeper, you’ve already lost.

If you can’t explain exactly how a piece of logic will behave when a network request takes 5 seconds instead of 50 milliseconds, you aren’t looking at a stable system, you’re looking at a race condition waiting for a heavy load to trigger it.

Documentation is a defensive tool—write down the assumptions you’re making about execution order, because the version of you that writes the code will always be more optimistic than the version of you that has to fix it at 2 AM.

The Cost of Being Wrong

The Cost of Being Wrong in concurrency.

At the end of the day, spotting a race condition isn’t about memorizing every possible threading edge case; it’s about developing a healthy sense of suspicion toward any piece of code that assumes it has exclusive access to a variable. You have to look for the gaps—the tiny, silent windows between a check and an action where the world can change underneath you. Whether you are hunting for shared mutable state or tracing how non-deterministic logic can spiral into a production outage, the goal is the same: stop treating concurrency as a theoretical problem and start treating it as a practical, inevitable risk. If you see a pattern that relies on timing being “just right,” you aren’t looking at a feature; you’re looking at a ticking time bomb.

I’ve spent enough hours in post-mortems to know that the most expensive bugs are the ones that feel like ghosts—they appear, they cause chaos, and then they vanish when you try to debug them. Writing code that is easy to read is good, but writing code that is obvious in its intent is what saves your sleep schedule. Don’t aim for the cleverest implementation of a lock or the most complex lock-free structure just to prove you can. Aim for the version that your tired, 2:00 AM self can understand without a manual. If you can’t clearly see how the data is protected, it probably isn’t.

Frequently Asked Questions

How do I tell the difference between a genuine race condition and just a piece of code that's naturally slow or non-deterministic?

The easiest way to tell is to look for the “interleaving.” If a piece of code is just slow, it’s predictably slow—it takes five seconds every single time. If it’s a race condition, the result is a moving target. You’ll see it in the logs: one run works perfectly, the next fails with a weird state, and the third time, it’s like the error never existed. If the outcome depends on the timing of an external event, you’ve found your culprit.

If I spot a potential race condition during a code review, what's the best way to prove it's actually a bug rather than just a theoretical edge case?

The temptation is to say, “It’s a race condition because it’s possible,” but that rarely convinces a busy dev. Instead, I try to map out the interleaving. I’ll write out the two execution paths: Thread A does X, then Thread B does Y, then Thread A tries to do Z with stale data. If you can show the specific sequence where the state becomes invalid, it stops being a “theoretical edge case” and becomes a predictable failure.

Are there specific patterns or "code smells" in certain languages—like Go's channels or Java's synchronized blocks—that should immediately trigger a red flag for me?

In Go, if I see a channel being closed in a loop or passed around without a clear owner, my internal alarm goes off. It’s usually a recipe for a panic. In Java, `synchronized` blocks that wrap entire methods or long-running I/O are huge red flags; they’re just contention magnets. I also look for “naked” shared variables in any language—if it’s not wrapped in an atomic or a mutex, it’s probably going to bite us.

Once I've identified a race condition in a PR, how do I suggest a fix that doesn't just solve the immediate crash but also avoids killing our system's throughput?

The temptation is to throw a global mutex around the offending block and call it a day. It works, but you’ll just trade a race condition for a massive bottleneck. Instead, I try to narrow the scope of the lock to the smallest possible critical section. If I can use atomic operations or a concurrent data structure instead of a heavy-handed lock, I do it. The goal is to protect the state without turning our service into a single-threaded queue.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.