venkatesh
№050 · SEP 18, 2026 · 3 MIN READ

30 iPhones in Stock, 44 Orders Confirmed

“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 🍅💀

30 iPhones in stock, 44 orders confirmed

The code looked fine:

  1. Read the stock.
  2. Check if it’s greater than 0.
  3. Update stock to stock - 1.
  4. 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.

copied!