The Flash Sale Concurrency Challenge
During a mega flash sale (e.g. 1,000 PlayStation 5 consoles offered to 500,000 concurrent shoppers), hundreds of thousands of requests hit the inventory service in the exact same second. Traditional relational database transactions cause catastrophic failures:
-- THE FLASH SALE KILLER: ROW LOCK CONVOY
BEGIN TRANSACTION;
SELECT stock FROM items WHERE id = 123 FOR UPDATE;
IF stock > 0 THEN
UPDATE items SET stock = stock - 1 WHERE id = 123;
END IF;
COMMIT;
When 50,000 concurrent database connections attempt to acquire a row lock on item 123, the database connection pool exhausts in milliseconds, lock waits pile up until queries hit lock-wait timeouts, and every other query that needs a connection stalls behind the hot row.
The Multi-Layer In-Memory Locking Architecture
Under the Hood: Atomic Redis Lua Script
Redis executes Lua scripts atomically in its single-threaded event loop. No other command can execute between reading the stock and decrementing it, eliminating race conditions with zero database locks:
-- Keys: 1 = item_stock_key, 2 = user_reservations_set
-- Args: 1 = user_id
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock <= 0 then
return -1 -- Sold out
end
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
return -2 -- Already reserved by this user
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1 -- Success: stock reserved
Delayed Release Timer: If the user does not complete payment within 15 minutes, a delayed scheduler (Redis ZSET) automatically re-increments the Redis stock and invalidates the reservation token.
Worked example: funnel sizing
1,000 units, 500,000 users arriving within 10 seconds.
- Edge or waiting room: admit users at a controlled rate, for example 5,000 per second, and send the rest to a queue page with a position number. This protects everything behind it.
- Reservation step: an atomic Redis Lua script checks per-user limits and decrements stock. At 5,000 attempts per second, all 1,000 units are reserved within about 0.2 seconds of opening. After that the script just returns SOLD_OUT, at tens of thousands of operations per second on one shard.
- Order persistence: 1,000 reservations are written to Kafka and then batch-inserted into Postgres. That is trivial load, where 500,000 concurrent
SELECT ... FOR UPDATEon one row would have been fatal. - Payment: 10-minute holds. If 15 percent abandon, 150 units return to stock at expiry and are re-offered to the waiting room.
The atomic reservation (Redis Lua)
-- KEYS[1]=stock:{sku1} KEYS[2]=buyers:{sku1} ARGV[1]=user_id
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return -2 end
local s = tonumber(redis.call('GET', KEYS[1]) or '0')
if s <= 0 then return -1 end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return s - 1Inventory strategies compared
| Strategy | Correctness | Throughput on one SKU | Complexity |
|---|---|---|---|
| SELECT FOR UPDATE then UPDATE | Strong | Hundreds per second (serialised on one row) | Low |
| Single atomic UPDATE with WHERE stock > 0 | Strong | A few thousand per second (still one hot row) | Low |
| Stock split into N bucket rows | Strong | N times higher | Medium: pick buckets, rebalance near zero |
| Redis Lua counter plus async DB | Strong at Redis, with a failover risk | Tens of thousands per second or more | Medium: reconciliation |
| Pre-generated tokens in a queue | Strong: each token is claimed once | High | Medium |
| Waiting room in front of any of the above | Unchanged | Protects backends | Adds a UX component |
How real systems do it
- Alibaba's Singles' Day uses layered throttling, cached stock checks, and database hot-row optimisations (queued updates on a hot row in AliSQL).
- Ticketmaster's Smart Queue and Cloudflare's Waiting Room meter admission so that the inventory system only sees a manageable rate.
- Shopify throttles checkout per shop and load-tests flash-sale merchants ahead of drops.