1. Blog
  2. System breakdown

System breakdown: how a flash sale doesn't melt the database

At 6 pm, 500 bags of rice go on sale at half price and thousands of people press Buy in the same few seconds. Here is how the system sells exactly 500, stays up for everyone else, and says no fast.

The Kolom team6 min read

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

Shoppers CDN Stock gate Queue Postgres 1. page views 2. buy 3. sold out 4. got a unit 5. steady pace most requests end here
Most of the crowd is looking, so the CDN answers them. Buys hit a counter in Redis. Only the 500 winners reach the queue, and Postgres sees a steady trickle of orders instead of a wall.
  1. 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.
  2. 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.
  3. "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.
  4. 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.
  5. 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:

SQL
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.

system designdatabasesscaling

Get the next one by email