On this page
What Is a Race Condition?
A race condition happens when the outcome depends on the timing of concurrent operations. Learn it with a concrete code example and the fixes.
Quick answer
- A race condition is a bug where the result depends on which of two concurrent operations finishes first.
- The classic example is a lost update: two processes read a value, both increment it, and one write overwrites the other.
- Fixes include atomic operations, locks, and transactions, depending on the data store.
What is a race condition?
A race condition occurs when the correctness of a program depends on the timing or ordering of two operations running concurrently. If both read and modify the same state without coordination, the interleaving determines the outcome — and one of the interleavings is wrong. The bug is intermittent by nature: it only appears when the timing lines up badly, which makes it one of the hardest classes of bug to reproduce.
A concrete example
Two users click “buy” on the last item in stock. Both processes run:
stock = db.get("item_count") # both read 1
if stock > 0:
db.set("item_count", stock - 1) # both write 0Both read 1, both pass the check, both write 0 — two orders for one item. The lost update is invisible until concurrency appears.
How to fix it
The fix depends on your store. A database transaction with row locking (or SELECT ... FOR UPDATE) serializes the read-modify-write. An atomic operation (UPDATE items SET count = count - 1 WHERE count > 0) does the whole thing in one statement. A compare-and-swap or optimistic locking (check a version before writing) detects conflicts and retries. The principle is the same: make the read-and-write one indivisible step, or detect when it wasn’t.
Where this bites vibecoders
Generated code is almost always written for the single-request happy path — the race only exists when two requests overlap, which the demo never shows. The habit is to ask, for every read-then-write, “what if two of these run at once?” Inventory, balances, and counters are the classic victims. If the answer isn’t safe, the write needs a transaction or an atomic operation, not a hope.
Where AI coding assistants get this wrong
- Writing read-check-write logic with no transaction or locking.
- Ignoring that even a “quick” check can interleave under concurrency.
- Using a cache as if it were a consistent store for counters.
- Testing single-threaded and concluding the code is correct.
Checklist
- Identify every read-then-write on shared state.
- Make each one atomic or transactional.
- Use row locks or optimistic version checks where needed.
- Test with concurrent requests, not just sequential ones.
- Treat “it worked once” as no evidence of thread-safety.
FAQ
Is a race condition only in multi-threaded code?
No. It also happens across processes, servers, and even separate users of a web app — anywhere two operations touch shared state concurrently. Web apps hit races constantly through concurrent HTTP requests, not just threads.
What is a lost update?
A lost update is the classic race outcome: two operations read the same value, both compute a new value from it, and the second write overwrites the first, losing one update. It’s the specific failure in the inventory example above.
What is optimistic locking?
Optimistic locking assumes conflicts are rare: each write includes a version or timestamp, and the update only succeeds if the version hasn’t changed since it was read. If it has, the operation detects the conflict and retries — avoiding the lost update without holding locks.