Race Condition
A race condition occurs when correctness depends on the unpredictable timing or ordering of concurrent operations accessing shared state.
Definition
A race condition is a defect in which the program outcome depends on the non-deterministic interleaving of operations that access shared state. Two threads may both read a counter, add one, and write back, losing an increment. The bug is timing-dependent: it may pass a thousand runs and fail under load. Unlike a data race — unsynchronised concurrent access to memory defined as undefined behaviour — a race condition is about logical correctness and can occur even with atomically safe primitives.
Context and scope
Pervasive in threads, async tasks, distributed services, and any code where shared mutable state is touched without coordination.
Common misconceptions
- Race condition equals data race — related but distinct; a race condition is a logical ordering bug and can appear even without memory corruption
- Adding locks everywhere removes races — locking solves the symptom but correct granularity and ordering must still be designed, or deadlocks appear
- It cannot be reproduced, so it is not real — non-determinism hides it, not eliminates it; it is real exactly when it matters
Anti-patterns
- Check-then-act sequences on shared state without holding a lock across both steps
- Assuming a timestamp or counter is monotonic across concurrent writers
- Fixing a race by adding a sleep instead of a proper synchronisation primitive
Related concepts
Related examples
Authoritative source
What it is
A race condition exists when the result of a program depends on which of several concurrent operations happens first. The classic example is two workers incrementing a shared counter: both read ten, both compute eleven, both write eleven, and one increment is lost. Nothing is corrupt in memory; the value is simply wrong because a read and a write were not atomic together.
Why it matters
Races are the hardest class of bug to find because they are intermittent. A test loop passes, a load test passes, then the value drifts in production under real concurrency. They appear in counters, caches, lazy initialisation, check-then-act patterns, and distributed state where a lock cannot simply be taken. Left unfixed, they cause silent data corruption, overcharging, or double submission.
How to prevent it
Make the whole read-modify-write atomic, not its parts: use a compare-and-swap, a mutex that covers every access, or a single-threaded actor that owns the state. Prefer immutable data and message passing to shared mutable state. When coordination is distributed, use idempotency keys and server-side atomic operations, and prove the fix by reasoning about interleavings rather than by running tests and hoping.