“Only 30 iPhone 18 Pro left” ⚡
10,000 people clicked Buy in the same second.
44 orders got confirmed.
Stock: 30.
So now we either cancel 14 orders… or deliver tomatoes in an iPhone box 🍅💀

The code looked fine:
- Read the stock.
- Check if it’s greater than 0.
- Update stock to stock - 1.
- Create the order.
Every step looks correct. So what’s wrong?
The problem: check-then-act
Each step is correct on its own. The bug is the gap between the steps.
Request A: read stock → 1
Request B: read stock → 1
Request A: 1 > 0 ✅ → set stock = 0 → create order
Request B: 1 > 0 ✅ → set stock = 0 → create order
Both requests read the same value before either of them wrote. Both passed the check. Both created an order. The last write wins, and the stock count no longer matches the number of orders.
This is a race condition. With 10,000 concurrent requests, the window between “read” and “write” is wide enough for many requests to slip through together.
The fix: make check and update one step
1. Atomic conditional update
Let the database do the check and the decrement together:
UPDATE products
SET stock = stock - 1
WHERE id = :id AND stock > 0;
If it updates 1 row, you got the item and can create the order. If it updates 0 rows, the item is sold out. No two requests can both win the last unit.
2. Pessimistic locking
SELECT ... FOR UPDATE locks the row, so other requests wait until the current transaction commits. It’s correct, but on a hot product every buyer queues up on one row.
3. Optimistic locking
Add a version column and update only WHERE version = :readVersion. If someone else changed the row first, retry or fail. Works well when conflicts are rare, not so well in a flash sale.
4. Redis DECR in front of the DB
For very high traffic, decrement stock atomically in Redis first and reject anything that goes below zero. Only the winners reach the database. You then need to reconcile Redis with the database and handle failed payments by releasing stock.
The lesson
Correct steps don’t make a correct flow. Under concurrency, the gap between reading a value and writing it back is where the bug lives.