A race condition is a defect in which the correctness of a program depends on the relative timing or interleaving of operations that run concurrently, such as threads, processes or asynchronous tasks. In concurrency, it arises when two or more flows of execution access shared state and at least one of them modifies it without adequate coordination.
Race conditions are notorious because they are nondeterministic: the same code may run correctly thousands of times and then fail under a slightly different load, scheduler decision or hardware configuration. They cause lost updates, corrupted data, crashes and security vulnerabilities, and they are among the hardest defects in programming to reproduce.
How a race condition occurs
Most race conditions come from operations that look atomic in source code but are several steps at the machine level. The statement counter = counter + 1 typically compiles into a read, an addition and a write. If two threads both read the value 41 before either writes, both write 42, and one increment is lost.
The section of code that touches shared state and must not be interleaved is called a critical section. A race occurs when a critical section is not protected, so the scheduler can switch between threads at any instruction boundary, or threads on different cores can run the steps truly in parallel.
Modern hardware adds another layer. Compilers and processors reorder memory operations for performance, and each core may see writes from other cores late. Without proper synchronization, one thread can observe a partially initialized object, which is why language memory models define happens-before relationships that only synchronization creates.
Data races and other forms of race conditions
The terms data race and race condition are related but not identical. A data race is a precise low-level condition defined by language memory models; a race condition is the broader logical defect. The table shows the main forms.
| Form | Definition | Typical consequence |
|---|---|---|
| Data race | Unsynchronized concurrent accesses to one memory location, at least one a write | Undefined behavior in C and C++; torn or stale values |
| Read-modify-write race | Two flows read, change and write the same value | Lost updates, wrong counters |
| Check-then-act race | A condition is checked and acted on in separate steps | Duplicate records, double spending |
| TOCTOU race | Time of check differs from time of use for a resource | Security holes in file access |
| Order violation | An operation runs before the one it depends on | Use of uninitialized data |
| Async interleaving race | Single-threaded tasks interleave at await points | Stale state in user interfaces and servers |
A program can be free of data races and still contain race conditions. Two transactions that each lock correctly but check and then reserve the last seat in separate locked steps can still sell the seat twice. Conversely, asynchronous programming in a single-threaded event loop has no data races, yet it can race at every await point.
Techniques that prevent race conditions
Prevention means either removing shared mutable state or making every access to it coordinated. The right technique depends on contention, performance requirements and how much state is shared.
| Technique | How it prevents races | Trade-off |
|---|---|---|
| Mutex or lock | Only one thread enters the critical section | Contention and risk of deadlock |
| Atomic operations | Hardware performs read-modify-write as one step | Works only for simple values |
| Compare-and-swap loops | Retry an update until no one interfered | Complex; possible livelock under contention |
| Immutable data | Shared values never change | Copies cost memory and time |
| Message passing | Each piece of state has one owner | Design shift toward actors or channels |
| Database transactions | Isolation levels and row locks serialize access | Lower throughput at strict levels |
Locks solve races but introduce their own hazard: if two threads acquire two locks in opposite order, they can wait for each other forever, which is a deadlock. Acquiring locks in a consistent global order and keeping critical sections short are the standard defenses.
Detecting and diagnosing race conditions
Because races depend on timing, ordinary testing finds them only by chance. Effective detection combines specialized tools with design reviews.
| Method | What it finds | Limitation |
|---|---|---|
| Dynamic race detectors | Data races observed at runtime, such as with ThreadSanitizer or the Go race detector | Only paths that actually execute; slows programs several times |
| Static analysis | Unprotected shared accesses in code | False positives and missed cases |
| Stress testing | Failures under high concurrency and repetition | Nondeterministic results |
| Deterministic schedulers | Explores many interleavings systematically | Limited to small components |
| Type systems | Rust ownership rules forbid data races at compile time | Logical races remain possible |
Symptoms that should raise suspicion of a race include failures that disappear when logging is added, tests that fail only on build servers with many cores, and values that are almost but not quite correct, such as a total that is occasionally a few units short.
Common misconceptions
- A single-core machine cannot have races: preemptive scheduling interleaves threads on one core as well.
- The volatile keyword makes code thread-safe: in Java it guarantees visibility, and in C and C++ it does not provide synchronization at all; neither makes compound operations atomic.
- Rare failures are acceptable: a race that fails once in a million operations fails many times a day at scale.
- Adding a sleep fixes it: delays only change probabilities and hide the defect.
- Thread-safe collections make code safe: two individually safe calls in sequence still form a check-then-act race.
The lasting lesson is that correctness must not depend on timing. Code is correct only when every possible interleaving produces an acceptable result, and parallelism amplifies any race that a design leaves open.
Put simply
A race condition happens when two parts of a program try to change the same thing at the same moment, and the result depends on which one happens to be faster. Most of the time things work, but sometimes the timing lines up badly and one change silently overwrites the other.
Picture two people editing the same shopping list from separate copies. Both see “milk”, one adds bread, the other adds eggs, and each saves their own copy. Whichever copy is saved last wins, and one item quietly disappears. Programs need rules, like taking turns, to stop this from happening.
In practice
An online event shop sells limited seats. The purchase handler reads the remaining seat count, checks that it is above zero, charges the customer and then decreases the count. During normal days this works, but when a popular show opens with several hundred buyers in the first minute, the shop sells more seats than the hall holds.
Investigation shows a check-then-act race: several requests read a count of 1 at the same time, all pass the check and all complete the sale. Load tests on a developer laptop never reproduced it because requests rarely overlapped there.
The team replaces the separate read and write with one atomic conditional update in the database, which decreases the count only if it is still above zero and reports whether a row changed. Overselling stops completely, and a stress test with 1,000 concurrent purchase attempts for 100 seats now confirms exactly 100 successful sales.