Articles

What Is a Race Condition? Concurrency Bugs Explained

A race condition occurs when a program's correctness depends on the unpredictable timing of concurrent operations. Why they happen and how to prevent them.

Chisato Chisato · · 4 min read
Dark-themed code editor showing source code

A race condition is a bug that occurs when the correctness of a program depends on the relative timing of two or more operations happening concurrently — and that timing isn’t guaranteed. When multiple threads, processes, or asynchronous tasks read and write shared state without coordination, the outcome can change from run to run depending on which one gets there first. The code “usually” works, which is exactly what makes race conditions dangerous: they pass casual testing and then fail unpredictably in production.

A minimal example

Consider two concurrent operations incrementing a shared counter:

counter = 0

Thread A: read counter (0)
Thread B: read counter (0)
Thread A: write counter + 1 (1)
Thread B: write counter + 1 (1)

Both threads read the same starting value before either writes back, so one increment is lost. The final value is 1 instead of the expected 2. Neither thread did anything wrong in isolation — read, add one, write is correct sequential logic. The bug only exists because the two sequences interleaved in a way that let one overwrite the other’s work.

This pattern — read, compute, write, with no guarantee the value didn’t change in between — is sometimes called a “check-then-act” or “read-modify-write” race, and it’s the most common shape these bugs take, whether it’s an in-memory counter, a bank balance, or a row in a database.

Why they’re hard to catch

Race conditions depend on timing, and timing is exactly the thing that’s hardest to control and reproduce in a test environment. A test suite that runs operations sequentially, or that happens to run fast enough that one operation always finishes before another starts, will never trigger the interleaving that causes the bug. The same code can pass thousands of test runs and then fail once under production load, when a slower network response or a busier CPU changes the timing just enough to expose the race.

This is also why race conditions are notoriously difficult to debug after the fact: adding a print statement or attaching a debugger changes the timing of the program, which can make the race condition disappear — sometimes called a “Heisenbug.” The observation changes the outcome.

Common places races show up

  • Shared in-memory state — global variables, caches, or counters accessed by multiple threads or async callbacks without synchronization.
  • Database rows — two requests reading a row, computing a new value, and writing it back, each unaware of the other’s write. This is why “select balance, then update balance” is a classic race in payment systems, and why databases offer atomic increments and transactions specifically to avoid it.
  • File systems — one process checking whether a file exists before creating it, while another process creates it in the gap between the check and the create (a “time-of-check to time-of-use,” or TOCTOU, race — a category with real security implications).
  • The event loop in JavaScript — JavaScript is single-threaded, but async code still has ordering hazards. Two fetch calls that both update the same UI state can resolve in a different order than they were issued, so the “last” response to arrive — not the last one requested — wins, unless you explicitly guard against it.

How to prevent them

Locks and mutexes. The classic fix: wrap the read-modify-write sequence in a lock so only one thread can execute it at a time. This eliminates the race but introduces its own risks — a lock held too long becomes a bottleneck, and locks acquired in inconsistent orders across different code paths can deadlock.

Atomic operations. Many languages and databases provide atomic primitives — INCREMENT, compare-and-swap — that perform a read-modify-write as a single, indivisible step at the hardware or database level. Where available, these are cheaper and safer than a general-purpose lock, because there’s no window for another operation to interleave.

Immutability. A value that’s never mutated after creation can’t be raced over. Concurrent code that passes around immutable data and replaces references instead of mutating shared objects removes an entire class of races by construction.

Message passing instead of shared memory. Rather than multiple workers touching the same variable, route all writes through a single owner that processes requests one at a time — the pattern behind actor models and message queues. Only one thing ever touches the state, so there’s nothing to race.

Transactions. Databases offer transactional isolation specifically so a sequence of reads and writes appears atomic to other concurrent transactions. Choosing the right isolation level is a trade-off between correctness guarantees and throughput, but it’s the standard defense against races on persistent data.

Race conditions vs deadlocks

These two are often confused because both are concurrency bugs, but they’re opposites in effect. A race condition produces a wrong result because operations interleave in an unintended order. A deadlock produces no result at all, because two or more operations are each waiting on a resource the other holds, and neither can proceed. Fixing one carelessly — adding a lock to stop a race — is a common way to introduce the other.

The takeaway

A race condition is a correctness bug rooted in timing: the program assumes an ordering of concurrent operations that isn’t actually guaranteed. They’re hard to find because standard testing rarely reproduces the exact interleaving that triggers the bug, and hard to debug because observing the program can change its timing. The fix is always some form of taking the ambiguity out of the ordering — locks, atomic operations, immutability, or funneling shared-state writes through a single owner — so the outcome no longer depends on who gets there first.

The Lycoris Team The Lycoris Team · · 4 min read

The Producer-Consumer Problem, Explained

The producer-consumer problem is a classic concurrency pattern: coordinating producers and consumers around a shared, bounded buffer safely.

#Computer Science #Concurrency #Programming
The Lycoris Team The Lycoris Team · · 4 min read

How Regular Expressions Work Under the Hood

Regular expressions are matched by finite automata or backtracking engines. How regex engines parse patterns, and why some patterns run slowly.

#Computer Science #Algorithms #Developer Tools
The Lycoris Team The Lycoris Team · · 4 min read

What Is Dependency Injection?

Dependency injection passes an object's dependencies in from outside rather than letting it construct them, making code easier to test and swap.

#Developer Tools #Backend #Computer Science