System Design · Traffic Spike Engineering

Design a Flash Sale System (Flipkart Big Billion Days)

10 million people, 50,000 phones, one second at noon. How to admit traffic fairly, never sell unit 50,001, keep bots out and degrade gracefully instead of falling over.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 15 min read
Crowd of shopping carts funnelling through gates toward a checkout counter with limited stock, representing a flash sale system
TL;DR
  • A flash sale is a funnel: millions of page views → hundreds of thousands of "Buy" clicks → tens of thousands of units. Design each layer to absorb its own load and pass only what the next layer can handle.
  • Serve product and countdown pages statically from the CDN; pre-warm caches and pre-scale everything before T-0, because autoscaling is too slow for a vertical spike.
  • Put a virtual waiting room in front of checkout and admit users at the rate checkout can sustain, with short-lived signed admission tokens.
  • Prevent overselling with an atomic, conditional decrement: a Redis Lua script as the fast gate, a SQL WHERE available >= qty update (split across bucket rows) as the durable guard, and TTL reservations that release abandoned stock.
  • Fight bots with per-account limits, verified pre-registration, rate limiting and CAPTCHA at entry. Shed non-critical features with flags so checkout survives.

Most system design problems are about steady-state scale. A flash sale is about a cliff: traffic goes from normal to 50–100x in the second the clock hits 12:00, almost all of it competing for a tiny amount of stock, and almost all of it destined to fail. The core skill is turning that chaos into an orderly, fair queue in front of a small, correct inventory system. This guide designs the "Big Billion Days" phone sale end to end, from the CDN down to the row lock.

The Shape of a Flash Sale

Think of the sale as a timeline, because the design has components active at different times:

TimeWhat users doWhat the system must do
T-7 days to T-1 hourRegister interest, add address and payment method, save cardsPre-registration, account verification, capacity tests
T-15 minOpen the product page, watch the countdown, refresh repeatedlyServe static pages from the CDN; absorb refresh storms
T-0 to T+60 sEveryone clicks "Buy Now"Admit fairly, reserve stock atomically, reject the rest fast
T+1 to T+15 minWinners pay; losers retry hoping for released stockProcess payments, expire abandoned reservations, re-release stock
T+15 min onward"Sold out"Short-circuit traffic at the edge; reconcile orders vs stock

The single most useful framing for the interview: 99%+ of requests at T-0 should fail, so make failing cheap. A "sorry, sold out" served from a CDN costs nothing; one that went through five services and a database row lock to find out is what takes systems down.

Requirements

Functional

  • Show product page and countdown; open purchasing at exactly T-0.
  • Let users reserve a unit, then pay within a time limit.
  • Enforce a per-user purchase limit (say 1 unit per account).
  • Release unpaid reservations back to stock.
  • Show a clear status: in queue (position), reserved (timer), sold out.

Non-functional

  • Never oversell. Selling unit 50,001 means cancelling an order, which is a worse experience than not getting one.
  • Don't undersell either: abandoned reservations must return to stock.
  • Stay up under the spike; the rest of the site (other categories) must keep working.
  • Fairness: real users should have a reasonable chance compared to bots and scripts.
  • Low latency for the reserve step (a user should know within a second or two whether they got it).

Traffic Estimates

Assumptions (illustrative, not company data):

  • 10 million users online for the noon phone deal; stock: 50,000 units.
  • In the last minute before T-0, each user refreshes every ~5 s → 10M / 5 = 2M page requests/s.
  • At T-0, say 40% click Buy within the first 10 s → 4M clicks / 10 s = 400K buy requests/s, plus client retries that can double or triple it.
  • Successful reservations: 50,000, almost all within the first few seconds → ~10K–25K successful reserves/s for a moment, then nothing.
  • Payment: 50K payments over ~10 minutes ≈ ~80 payments/s average with an early peak of a few hundred per second.
~2M/s
Page loads (edge)
~400K/s
Buy clicks at T-0
50K
Units that can succeed
~1%
Buy clicks that can win

Read that last number carefully: of 4 million buy attempts, about 1 in 80 can succeed. Checkout, payment and order systems only need to handle the winners. Everything upstream exists to reject the other 79 quickly and fairly.

Architecture: A Funnel with Gates

10M users | v +-----------------------------+ Gate 0: CDN serves static product page, | CDN / Edge (WAF, bot score, | countdown JS, "sold out" flag JSON. | rate limit, static pages) | Blocks obvious bots and floods. +--------------+--------------+ | buy clicks (~400K/s) v +-----------------------------+ Gate 1: waiting room. Assigns queue | Waiting Room service | position, admits N users/s, issues | (stateless + Redis counters)| signed admission tokens. +--------------+--------------+ | admitted (~5-20K/s) v +-----------------------------+ Gate 2: atomic reserve in Redis | Reservation service | (Lua): stock check, per-user limit, | Redis {sku} counters | reservation with TTL. +------+---------------+------+ | reserved | events (Kafka) v v +---------------+ +-------------------------+ Gate 3: durable, | Checkout & | | Inventory DB (SQL) | conditional decrement | Payment | | bucketed stock rows, | on bucketed rows. | (isolated) | | reservations table | +-------+-------+ +-------------------------+ | v +---------------+ +------------------------+ | Order service | | Expiry worker: releases| | (confirm) | | unpaid reservations | +---------------+ +------------------------+

Each gate reduces load by an order of magnitude. The services behind gate 1 never see more traffic than they were sized for, regardless of how many people show up. That is the whole trick.

Gate 0: Static Content, Cache Warming and Pre-Scaling

  • Make the product page static. Price, images, specs and description are fixed for the sale; render them once, publish to the CDN with a long TTL. Personalised bits (cart count, user name) load via a separate small API call that can fail without breaking the page.
  • Countdown in the client using server time fetched once (not a per-second poll). The "Buy" button becomes active at T-0 based on that offset, and the server enforces T-0 independently, because clients can be tampered with.
  • Sale state as a tiny edge-cached JSON ({"state":"live","sold_out":false}, TTL 1–2 s). When stock runs out, flip it; within seconds every client stops sending buy requests.
  • Pre-warm caches: load SKU details, price, stock counters, user addresses and saved payment tokens for pre-registered users into Redis before T-0. A cold cache at T-0 means a thundering herd on the database (the classic cache stampede).
  • Pre-scale: scale out stateless tiers to the forecast peak an hour before; warm connection pools; pre-split hot database and cache partitions. Reactive autoscaling takes minutes; the spike takes one second.
  • Load test the real thing: full-path tests with production-like data, including the failure paths (payment gateway slow, Redis node failover).

Gate 1: Queue-Based Admission (Virtual Waiting Room)

If 400K requests/s hit the reservation service directly, most get timeouts, users retry, and the retry storm makes it worse. A waiting room converts the burst into a steady, controlled stream.

  1. Enter. At T-0 the "Buy" click calls POST /waiting-room/{sale}/join. The service does an atomic INCR sale:{id}:seq in Redis and returns a signed ticket with the user's position.
  2. Randomise the first window (optional). Everyone who joined between T-0 and T+10 s is effectively simultaneous; network latency, not intent, decided their order. Shuffle positions within that window so being 40 ms closer to the data centre doesn't win. This is a product decision, mention both options.
  3. Admit at a fixed rate. A counter sale:{id}:admitted_upto advances by R every second (R = what checkout can handle, say 10K/s). Clients poll GET /status every few seconds (served from cache) or listen for a push.
  4. Issue an admission token when position ≤ admitted_upto: a JWT/HMAC token with user ID, sale ID and expiry (2 minutes). The reservation API rejects any request without a valid, unused token.
  5. Stop early. When remaining stock + expected releases is clearly less than the number of admitted-but-undecided users, stop admitting and tell queued users the item is sold out (with a "we'll notify you if stock returns" option).
💡
Size the admission rate from the bottleneckR should be set from the slowest component behind the gate (often payment initiation or the database), with headroom. If checkout can do 15K reserves/s, admit 10K/s. The waiting room is rate limiting with a queue and a fairness policy.

This same pattern is used for railway Tatkal bookings and concert ticket sales; see IRCTC Tatkal design for a version with a seat-allocation twist.

Gate 2: Inventory Reservation

Separate reserving a unit from buying it. A reservation holds one unit for a user for a fixed time (say 10 minutes) while they pay. Stock has three buckets:

total = available + reserved + sold reserve: available -1, reserved +1 (atomic, conditional) confirm: reserved -1, sold +1 (on payment success) expire: reserved -1, available +1 (TTL passed, no payment)

The Redis reserve script

All keys for one SKU share a Redis Cluster hash tag ({sku:123}) so the script runs atomically on one shard. Redis executes a Lua script without interleaving other commands, so check-then-decrement is safe.

Python + Lua (redis-py)
import time, uuid, redis

r = redis.Redis(decode_responses=True)

RESERVE = r.register_script("""
-- KEYS[1] stock, KEYS[2] per-user marker, KEYS[3] reservations zset
-- ARGV[1] qty, ARGV[2] reservation_id, ARGV[3] expires_at (epoch s)
if redis.call('EXISTS', KEYS[2]) == 1 then
  return -2                                   -- user already holds/bought one
end
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
local qty = tonumber(ARGV[1])
if stock < qty then
  return -1                                   -- sold out
end
redis.call('DECRBY', KEYS[1], qty)
redis.call('SET', KEYS[2], ARGV[2], 'EX', 86400)
redis.call('ZADD', KEYS[3], ARGV[3], ARGV[2])
return stock - qty                            -- remaining
""")

RELEASE = r.register_script("""
-- KEYS[1] stock, KEYS[2] per-user marker, KEYS[3] reservations zset
-- ARGV[1] reservation_id, ARGV[2] qty
if redis.call('ZREM', KEYS[3], ARGV[1]) == 1 then
  redis.call('INCRBY', KEYS[1], tonumber(ARGV[2]))
  if redis.call('GET', KEYS[2]) == ARGV[1] then
    redis.call('DEL', KEYS[2])
  end
  return 1
end
return 0                                      -- already confirmed or released
""")

def keys(sku, user):
    tag = "{sku:%s}" % sku
    return [f"{tag}:stock", f"{tag}:user:{user}", f"{tag}:resv"]

def reserve(sku, user, hold_seconds=600):
    rid = str(uuid.uuid4())
    res = RESERVE(keys=keys(sku, user), args=[1, rid, int(time.time()) + hold_seconds])
    if res == -1: return {"status": "sold_out"}
    if res == -2: return {"status": "limit_reached"}
    publish("reservation_created", sku=sku, user=user, rid=rid)  # to Kafka -> DB
    return {"status": "reserved", "reservation_id": rid, "hold_seconds": hold_seconds}

def release_expired(sku, now=None):
    now = now or int(time.time())
    tag = "{sku:%s}" % sku
    for rid in r.zrangebyscore(f"{tag}:resv", "-inf", now, start=0, num=500):
        user = lookup_user(rid)                  # from the reservations table
        if RELEASE(keys=keys(sku, user), args=[rid, 1]) == 1:
            publish("reservation_expired", sku=sku, rid=rid)

On payment success, the confirm path removes the reservation from the ZSET (ZREM) so the expiry worker can't release it, and records the sale. The ZREM return value is the arbiter: whichever of "confirm" or "expire" removes the entry first wins, and the other becomes a no-op. If expiry wins a race against a payment that succeeded at the very last second, the order path must refund or, better, try to take another unit; give a small grace period on expiry to make that rare.

Gate 3: Oversell Prevention You Can Trust

Redis is fast but its replication is asynchronous. If the primary fails right after acknowledging 300 decrements that hadn't reached the replica, the promoted replica has 300 extra units. That's an oversell. So the in-memory counter is a filter, and the database is the guard.

Durable conditional decrement

SQL
-- Stock split into N bucket rows to avoid a single hot row
CREATE TABLE inventory_bucket (
  sku_id     BIGINT,
  bucket     SMALLINT,
  available  INT NOT NULL CHECK (available >= 0),
  PRIMARY KEY (sku_id, bucket)
);
-- 50,000 units across 20 buckets = 2,500 each

-- Reserve one unit from a randomly chosen bucket
UPDATE inventory_bucket
   SET available = available - 1
 WHERE sku_id = 123 AND bucket = :b AND available >= 1;
-- 1 row updated  => success; insert reservation row in the same transaction
-- 0 rows updated => bucket empty; try another bucket (or report sold out
--                   once every bucket has returned 0)

Why buckets? A single row is serialised by its row lock: every transaction waits for the previous one to commit. At a few milliseconds per commit, one row tops out at a few hundred to low thousands of updates per second. Twenty bucket rows give roughly 20x the throughput, and the CHECK (available >= 0) constraint plus WHERE available >= 1 make negative stock impossible regardless of concurrency.

Putting the layers together

  1. Redis Lua says yes (≈ 50K yeses total, everyone else gets "sold out" from memory).
  2. The reservation service performs the SQL bucket decrement and inserts the reservation row in one transaction. Only ~50K such transactions happen, spread over a few seconds: comfortable for a sharded SQL cluster.
  3. If SQL says no (Redis drifted after a failover), return "sold out" and correct the Redis counter.
  4. A reconciler compares Redis stock, SQL buckets, and reservation/order counts every few seconds during the sale.

Alternatives worth naming: pre-generated stock tokens (push 50,000 unit IDs into a queue/list; a reserve is an atomic pop, so it can't go below zero by construction), and pessimistic SELECT ... FOR UPDATE (correct but serialises everything on one lock). The token approach is elegant for small, fixed stock; the bucket approach scales to large catalogues with many SKUs.

Checkout and Payment

  • Isolate checkout: dedicated instances, connection pools and database shards for the sale SKUs so a meltdown in browsing can't starve checkout, and vice versa (bulkhead pattern).
  • One-tap payment: pre-registered users have saved addresses and payment methods (tokenised cards, UPI handles), so the reserved window isn't wasted typing addresses.
  • Idempotent order creation: the reservation ID is the idempotency key for the order. Double-tapping "Pay" creates one order. The detailed payment state machine is in payment system design.
  • Pending payments: a UPI payment can be pending for minutes. Extend the reservation while the payment is in a pending state rather than releasing stock under a user who has actually paid.
  • Payment gateway capacity: coordinate with gateways/banks beforehand; spread across multiple gateways; the admission rate must respect the payment provider's limits too.

Bot Protection and Fairness

Resellers run scripts that click at exactly T-0 from hundreds of accounts. Without defences, they win. Layer them:

LayerTechniqueStops
Before the saleMandatory pre-registration; account age and phone/OTP verification; KYC-lite checksFreshly created bot accounts
EdgeWAF rules, IP reputation, rate limit per IP/ASN, TLS/device fingerprintingFloods, data-centre scripts
Waiting room entryCAPTCHA or proof-of-work challenge, shown before T-0 so it doesn't add latency at T-0Headless automation at scale
AdmissionRandomised order within the opening windowLatency-optimised scripts
ReservationOne unit per account, device, address and payment instrument (enforced atomically)Multi-account hoarding
Post-saleCancel orders linked by shared address/payment/device graphRings that slipped through

Per-user limits must be enforced in the same atomic step as the stock decrement (the per-user key in the Lua script); checking in a separate call lets a user's ten parallel requests all pass the check before any of them records the purchase.

Graceful Degradation: What to Turn Off

Every dependency on the sale path is a liability. Decide in advance, behind feature flags, what to switch off when pressure rises:

  1. Personalisation and recommendations ("customers also bought") → static lists.
  2. Reviews, Q&A, seller details → served stale from cache or hidden.
  3. Real-time stock counts ("only 312 left!") → coarse states ("selling fast", "sold out") updated every few seconds.
  4. Non-critical writes (analytics events, wishlist updates) → buffered on the client or dropped.
  5. Coupons and complex pricing → fixed sale price only, computed beforehand.
  6. Last resort: close the waiting room entrance and show a "high demand, you'll be notified" page from the CDN. A controlled "please wait" beats an uncontrolled 5xx page.

Use circuit breakers on every downstream call with sensible fallbacks, and make client retry behaviour sane: exponential backoff with jitter, a cap on retries, and the server returning Retry-After. A million clients each retrying three times in the same second is a self-inflicted DDoS.

Sale-Day Runbook

T-7 days

Prepare

Load tests at 2x forecast, game days for Redis/DB failover, payment gateway coordination, pre-registration opens.

T-1 hour

Warm

Scale out, warm caches and pools, publish static pages, load stock into Redis and SQL buckets, freeze deploys.

T-0

Watch

Dashboards: admission rate, reserve success/fail, Redis vs SQL stock drift, payment success, error rates, p99 latency.

T+30 min

Reconcile

Sold + reserved + available = initial stock; cancel fraud; re-release expired units; post-mortem.

Interview Angle

What interviewers listen for

  • You quantify the funnel and say "most requests should fail cheaply".
  • You separate reserve from buy and handle expiry.
  • You make stock decrement atomic and conditional, and you know why Redis alone isn't a durable guarantee.
  • You address the hot row (buckets or token queue).
  • You bring up bots and fairness without being prompted.

Follow-ups to rehearse

  • "Redis primary dies at T+3 s. What happens?" Replica promoted, possibly missing recent decrements; SQL guard prevents oversell; reconciler fixes the Redis counter; some users see "sold out" a bit later than ideal.
  • "How do you show queue position to 4M users?" Position is computed once at join; status polls hit a cached admitted_upto value, so it's one cheap read per poll, served at the edge if needed.
  • "Multiple SKUs (colours/storage variants)?" Each SKU has its own counters and hash tag; the waiting room can be per sale event rather than per SKU.
  • "How would you do this for movie seats?" Seats are distinct, not fungible: reserve specific seat IDs with per-seat locks, as in BookMyShow design.

Common mistakes

  • "Autoscaling will handle it."
  • SELECT stock then UPDATE stock = stock - 1 in two steps (classic race, oversells).
  • Using a distributed lock around the whole checkout per SKU (serialises the sale to one user at a time).
  • No reservation expiry, so abandoned carts keep stock hostage until the sale is over.
  • Letting the flash sale share the database pool with the rest of the site.

The flash sale combines rate limiting, caching, atomic counters and payments under extreme concurrency, which is why it's a favourite at Indian e-commerce companies. Practise drawing the funnel and the three stock states; they anchor every follow-up. For the caching layer specifically, Redis data structures and use cases and distributed cache design go deeper.

Frequently Asked Questions

How do you prevent overselling in a flash sale?▾
Make the stock decrement atomic and conditional so it can never go below zero, for example a Redis Lua script that checks and decrements in one step, or a SQL update with WHERE available >= qty. Combine a fast in-memory gate with a durable conditional update in the database, and reconcile the two, so a cache failover cannot cause overselling.
What is a virtual waiting room in e-commerce?▾
A virtual waiting room holds users in a queue before they can enter a high-demand flow such as checkout. Users receive a position and are admitted at a controlled rate that matches backend capacity, each admitted user getting a short-lived signed token that the checkout service requires. It protects the system and makes access fairer than a free-for-all.
How long should an inventory reservation last?▾
Long enough for a typical user to pay and short enough that abandoned carts return stock quickly, often in the range of 5 to 15 minutes for flash sales. When a reservation expires without a confirmed payment, a background process returns the units to available stock and they become purchasable again.
How does Flipkart handle Big Billion Days traffic?▾
Flipkart has spoken publicly about extensive load testing, capacity planning and preparing systems for sale traffic, but detailed internals are not fully public. The general techniques used across large e-commerce sales are pre-scaling, CDN-served static pages, cache warming, admission control, atomic inventory counters, feature flags to shed non-critical features, and isolating checkout from browsing.
Why not just use a database transaction to decrement stock?▾
You should use one as the final guard, but a single inventory row receiving tens of thousands of conditional updates per second becomes a lock hotspot, and most of those requests will fail anyway once stock runs out. Filtering traffic in an in-memory counter and splitting stock across several rows keeps the database within its capacity.
How do you stop bots from buying all the stock?▾
Layer defences: require logged-in, verified accounts registered before the sale, enforce one unit per account and device, rate limit by account, device and IP, use CAPTCHA or proof-of-work at waiting room entry, score requests with device fingerprinting and behaviour signals, and randomise admission order among users who arrive at the start so raw speed matters less.

Prepare for High-Scale E-commerce Design Rounds

Prepflix's system design course breaks down Flipkart, Swiggy and IRCTC-style traffic spikes with mock interviews and feedback.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.