A race condition is a bug where the correctness of your program depends on the order or timing of things you do not control. Two operations run concurrently, they interleave in a way you did not anticipate, and the result is wrong — sometimes. The "sometimes" is what makes these bugs infamous: they pass in testing and detonate in production.
What changed in 2026
- Compiler-enforced safety spread. Rust-style ownership models and stricter concurrency linters in other languages now catch a large class of data races before the code ever runs.
- Async everywhere raised the stakes. As more code became concurrent by default through async runtimes, logical races over shared state — not just low-level data races — became the more common failure mode.
- Deterministic testing tools improved. Schedulers that systematically explore interleavings, plus better thread sanitizers, made once-invisible races reproducible in CI.
How a race condition happens
The canonical example is a shared counter. Two threads both run count = count + 1, which is really three steps: read count, add one, write it back.
Thread A: read count (5)
Thread B: read count (5)
Thread A: write 6
Thread B: write 6 // lost update: should be 7
Both threads read 5 before either wrote, so one increment vanishes. Nothing here is "wrong" in isolation — the bug lives entirely in the interleaving. Run it a million times and it may be fine; run it under the wrong load and you lose updates silently.
Data races vs logical races
Not every race is a low-level data race. It helps to separate them.
- A data race is two threads accessing the same memory concurrently with at least one write and no synchronization. This is undefined behavior in many languages.
- A logical race (or race condition on state) is higher level: a check-then-act sequence like "if the file does not exist, create it" where another process acts between the check and the act. Each operation may be individually safe, yet the sequence is not atomic.
You can eliminate all data races and still have logical races. The famous "time of check to time of use" security bugs are logical races.
Prevention strategies compared
| Strategy |
How it prevents races |
Cost |
| Mutex / lock |
Only one thread in the critical section at a time |
Contention, risk of deadlock |
| Atomic operations |
Hardware-level indivisible read-modify-write |
Limited to simple operations |
| Immutability |
No writes means nothing to race on |
Copying overhead |
| Message passing |
State owned by one actor; others send messages |
Architectural change |
| Single-threaded + async |
One thread, cooperative scheduling |
Still needs care across await points |
The most reliable fix is usually to remove the sharing, not to guard it. Immutability and message passing sidestep the problem instead of managing it. If your concurrency comes from async rather than threads, review how await points can interleave — the model is explained in synchronous vs asynchronous programming.
Why they are so hard to catch
Races are nondeterministic. The failing interleaving might occur in one run out of ten thousand, and only under production timing, load, and hardware. A debugger changes timing enough to hide the bug — the classic "heisenbug." This is why prevention beats detection: design out the shared mutable state rather than hoping tests find every interleaving.
Common pitfalls
Papering over with sleeps. Adding a delay so a race "usually" resolves correctly is not a fix. It is a slower bug.
Locking too little or too much. Too little leaves gaps; too much creates contention and deadlocks. A race and a deadlock are opposite failure modes of the same shared-state problem.
Assuming a single statement is atomic. count++ is not atomic in most languages. Neither is a 64-bit write on some platforms.
Non-atomic check-then-act. "Check it exists, then use it" is a race unless the two steps are made atomic together.
FAQ
What is the difference between a race condition and a deadlock?
A race condition produces a wrong result because operations interleaved badly. A deadlock produces no result because threads are stuck waiting on each other. Both stem from coordinating shared state, but they are opposite symptoms.
Are race conditions only a threading problem?
No. Multiple processes, distributed systems, async tasks, and even signal handlers can race. Any time two flows of control touch shared state without coordination, a race is possible.
Does a garbage-collected or memory-safe language prevent races?
Memory safety prevents some low-level data races but not logical races over application state. You can absolutely write a race condition in a fully memory-safe language.
How do I reliably reproduce a race?
Use thread sanitizers, stress tests with many concurrent workers, and schedulers that deliberately explore interleavings. Do not rely on it failing naturally — that is how it reaches production.
Where to go next