Every Thursday at 6 pm the Mangrove app runs a flash offer. This week it is 500 bags of 5 kg rice at half price. A push notification goes out at 5:55. At 6:00 a crowd opens the product page and presses Buy within the same few seconds.
Two things can go wrong. The database can fall over under the rush and take the whole app down with it, including everyone who only wanted bread and eggs. Or it stays up and sells 540 bags of a product it has 500 of. The first is an outage. The second is 40 angry customers and a round of refunds.
Here is how the traffic flows, and where each part of the design takes pressure off.
Why a sale is harder than a busy day
On a festival day Mangrove handles around four times its normal 60,000 app orders. That is a lot, but it is spread over many hours. A flash sale is different in two ways:
- The traffic arrives at once. What matters is not orders per day but requests in the first ten seconds.
- Everyone wants the same row. Every buy tries to change one number: the stock of one product. A database handles many writes to different rows in parallel, but writes to the same row wait in line for its lock.
The second point is the one that surprises people. Adding app servers does not help if every one of them is waiting on the same lock.
The path of a buy
- The product page comes from the CDN. Most people in the rush are looking, not buying, and many refresh to see whether the sale has started. The page, the photo and the countdown can all be cached at the edge, so those requests never reach Mangrove's servers. Caching explains what to cache and for how long.
- Buy goes to a stock gate. Before touching Postgres, the API asks Redis to take one unit: an atomic decrement on a counter that starts at 500. Redis runs commands one at a time in memory, so two requests can never both take the last unit.
- "Sold out" is answered right there. Once the counter reaches zero, every other buy gets "Sold out" in a few milliseconds. This is the most important step: in a big sale most buy requests end here, and they cost almost nothing.
- Winners go into a queue. The people who got a unit see "You got one, finishing your order", and their order goes on a queue.
- Workers write orders at a pace the database can take. Postgres sees a steady flow of 500 orders over a minute or two instead of thousands of writes in one second. Queues, background jobs and pub/sub shows how workers drain a queue safely.
The counter must not lie either
The gate moves the race from Postgres to Redis. That only works if the counter stays right, so there are three rules.
- Take, then give back. If an order fails later (the payment is declined, or the shopper closes the app at checkout), the worker returns the unit with an increment. Otherwise the sale ends with "Sold out" and 20 bags still sitting in the warehouse.
- Postgres stays the record. Workers still lower the stock in the database with a guarded update. If it changes no rows, the order is refused. Redis is a fast filter in front of the truth, not the truth.
- Retries carry a key. Phones retry on bad connections. An idempotency key per buy makes sure one person's three retries take one unit, not three.
The guarded update is one line:
UPDATE products SET stock = stock - 1
WHERE id = 77 AND stock > 0; -- 0 rows changed means sold out
Case study: ordering without overselling stock compares this with row locks and reservations in Postgres alone, and shows when each one is enough.
Slowing the crowd on purpose
Even "Sold out" answers cost something when the crowd is large. For the biggest sales Mangrove adds two more layers in front.
- A rate limit per account and per device. A script that presses Buy 50 times a second is turned away at the edge, before it uses any capacity. Rate limiting and protecting your system explains the token bucket behind this.
- A waiting room. When traffic passes a set level, new arrivals see "You're in line" and are let in at a fixed rate. It feels slower, but it is fair, and it keeps the rest of the app working for normal grocery orders.
The trade-offs
| Choice | You gain | You pay |
|---|---|---|
| Stock gate in Redis | Postgres never sees the rush; "sold out" is instant | Two counts to keep in step, and give-back logic |
| Queue for order writes | The database works at its own pace | Winners wait a few seconds for "confirmed" |
| Waiting room | The whole app stays up | Some people wait, and some leave |
| Postgres with a guarded update only | One source of truth and simple code | Fine for small sales; in a big one, the lock queue becomes the outage |
There is no shame in the last row. For a sale of 50 items to a few hundred people, a guarded update in Postgres is enough and far simpler. Add the gate and the queue when a quick estimate says the crowd will be larger than one row's lock can serve, not because a flash sale sounds like it needs them.
Want to feel the failure from the other side? The Flash sale meltdown incident drill (in the Plus drill library) puts you on call while a sale like this one goes wrong.