Concurrency control
Making read-decide-write sequences safe when other actors may change the same data in between: locks, conditional writes and constraints.
Concurrency
Learn it
A lot of code reads something, makes a decision, then writes: "if the seat is free, book it". Each step is fine on its own. The trouble starts when two requests run at the same time and their steps interleave:
Request A Request B SELECT seat 12 → free SELECT seat 12 → free UPDATE seat 12 owner = A UPDATE seat 12 owner = BCheck
After that sequence, who has seat 12, and who was told they booked it?The fix is to make read-decide-write act as one step. Three common ways:
- Lock the row:
SELECT … FOR UPDATE. B waits until A commits, then sees the seat is taken. - Conditional write:
UPDATE seats SET owner = 'A' WHERE id = 12 AND owner IS NULL, then check how many rows changed. Zero means someone got there first. - Constraint: a unique index on (show, seat) in a bookings table. The second insert fails.
- Lock the row:
Check
The conditional UPDATE reports 0 rows changed. What should the code do?Check
Which approach still protects you from an admin script someone writes next year without reading your booking code?Rules of thumb: use a constraint for "no two X may share Y"; use conditional writes when conflicts are rare; use a lock for a busy row with short updates, and never hold one across a slow network call.
Quick reference
The same ideas, condensed for revision.
How it goes wrong
- Check-then-act
- Read and write are separate statements with nothing preventing interleaving.
- Lost update
- Two writers read, modify and write back full rows; the second silently overwrites the first.
- Deadlock
- Two transactions lock rows in opposite orders and wait on each other forever (until the database kills one).
- Locks held across network calls
- A slow dependency turns every waiter into a timeout.
Instead, consider
- Single writer
- Route all writes for a key to one process or thread, so concurrent access cannot happen. See [[partitioning]].
- Commutative operations
- The operations can be applied in any order with the same result (increments, CRDTs).
- Serializable isolation
- Invariants span many rows and you prefer the database to detect conflicts and abort.
In practice
- SELECT … FOR UPDATE / SKIP LOCKED
- Row locks; SKIP LOCKED lets job claimers bypass rows being claimed.
- Version columns / ETags
- Optimistic concurrency with conditional writes.
- Unique and check constraints
- Invariants enforced by the database for every writer.
- Compare-and-set in key-value stores
- Redis WATCH, DynamoDB condition expressions, etcd transactions.
It assumes
- All writers go through the same mechanism; one unguarded path breaks the invariant.
- Conflict handling (retry, reject, merge) is defined for the losing side.
Explain it in your own words
Where you practise it
- A URL shortener like bit.ly
Generate short codes · Debug: a customer got someone else's link
- A reliable video processing pipeline
- Rate limiting a public API
Where does the count live? · The limiter that leaks · Write the atomic bucket
- Notifications across email, push and in-app
- A payment workflow that never double-charges
- A look-aside cache at Facebook's scale
- Storing trillions of chat messages
- Live queries: screens that update in real time
The task that never appeared · Seven tasks in a column of five · Write the commit check · Defend building it into the database
- Sharding Postgres while it is running
Shards that are slightly in the past · Queries that cross shards
Further reading
Engineers describing it in systems they run.
- How Convex Works
Convex · Sujay Jayakar · Post, Apr 2024
A walk through a reactive database from the inside: the transaction log, read sets, optimistic concurrency, and how a write finds the subscriptions it affects.
Related concepts
- Transactions
Grouping several reads and writes so they take effect all together or not at all, isolated from concurrent work to a defined degree.
- State machines for business state
Modelling an entity's lifecycle as explicit states and allowed transitions, enforced with conditional updates so concurrent or stale actors cannot corrupt it.
- Leases and fencing tokens
Ownership that expires unless renewed, plus a token that lets the rest of the system reject an owner that has lost its claim without knowing it.
- Idempotency
Designing an operation so that performing it twice has the same effect as performing it once, which is what makes retries safe.
- Generating unique identifiers
Making ids that are unique across machines and time, and choosing what else they reveal: order, volume, guessability, length.