Concurrency
"What if two actors modify the same state?"
Applies To
inventory, balances, enrollments, token refresh, counters
Why It Happens
A first-principles walkthrough of what happens when two actors read the same row at the same instant. The lost-update — both read seats=1, both write 0, one sale disappears. Why read-modify-write is never safe without a lock, and how SELECT FOR UPDATE and versioned optimistic locking make the check atomic at the database, not in application memory.
How It Works Underneath
Concurrency is two actors reading the same row at the same instant. Each reads seats=1, each checks if seats>0, each writes seats=0. One sale consumed two seats, one customer is oversold, and no error was thrown. The database executed both writes correctly — the bug is that the check and the write were not atomic. The fix is to make the check part of the write: UPDATE seats SET seats=seats-1 WHERE seats>0 and inspect rows_affected, or SELECT ... FOR UPDATE to lock the row between read and write, or a version column with WHERE version=expected.
T2: READ seats=1 ─┼─ both see 1, both write 0 → lost update
Fix: UPDATE ... WHERE seats>0 → second UPDATE affects 0 rows → 409
Common Misconceptions
“My language is single-threaded, so no race.” Concurrency is not threads — it is two HTTP requests from two users hitting two server processes at the same millisecond. The race is in the database, not in your runtime.
Cataloged Failure Modes
lost_updatedouble_enrollmentrace_on_write
Code Comparison
# NAIVE: Read-modify-write race condition
async def purchase_ticket(event_id: str):
event = await db.fetch_one("SELECT seats_left FROM events WHERE id = %s", event_id)
if event['seats_left'] > 0:
await db.execute("UPDATE events SET seats_left = %s WHERE id = %s", event['seats_left'] - 1, event_id)
return True
return False
# IMPROVED: Optimistic locking with atomic condition
async def purchase_ticket(event_id: str):
res = await db.execute("UPDATE events SET seats_left = seats_left - 1, version = version + 1 WHERE id = %s AND seats_left > 0", event_id)
if res.rows_affected == 0:
raise HTTPException(409, "No seats available or concurrent conflict")
return {"status": "confirmed"}