40. Ticketing & Flash Sales (Ticketmaster)
Business context. A huge on-sale event: 2M fans for 50k seats. The outcomes are fairness and trust (bots and crashes produce headlines), sell-through, and no overselling. The demand spike is 100–1000× the normal level for 30 minutes.
Requirements. Browse the event; join the sale; pick seats or best available; hold them for ~10 minutes; pay; get the ticket. Never sell a seat twice. Survive 2M concurrent users. Fair ordering.
Design.
Users ─▶ CDN (static event page; fully cached) ─▶ Virtual waiting room (queue service)
assign random/arrival-order position → token; admit at the rate the backend can sustain
─▶ Admitted users (signed admission token) ─▶ Seat map service (cached snapshot, refreshed per second)
─▶ Reserve: Inventory service — atomic hold:
UPDATE seats SET status='HELD', hold_id=?, expires_at=now()+10m
WHERE event_id=? AND seat_id IN (...) AND status='AVAILABLE' -- all-or-nothing check
(or Redis Lua per-section counters for "best available" GA tickets, persisted to DB)
─▶ Checkout (payment §39, idempotency) ─▶ CONFIRM hold → SOLD (conditional on hold_id & not expired)
─▶ Hold expiry: TTL sweeper releases expired holds back to inventory
Deep dives.
- The waiting room is the key architectural element. It converts an unbounded spike into a controlled admission rate, protects every downstream system, and is where fairness is implemented (randomized positions for everyone who arrived before the start, bot checks, one position per verified account).
- Inventory contention: row-level conditional updates on seats; partition by event section to spread lock contention. For general admission, treat inventory as counters in shards (e.g. 10 shards × 5,000 tickets), decremented atomically, so contention is split across shards.
- Holds = TCC (§12.3): a reservation with expiry. The expiry must be enforced at confirm time (a conditional update), not only by the sweeper, or a late payment could confirm a seat that was already released and resold.
- Bots: verified fan programs, rate limits per account/device/IP, CAPTCHAs at the queue entrance, purchase limits per account.
Strategic lens. Capacity: don't size the whole stack for 2M concurrent users. Size the waiting room for 2M (cheap, static, CDN-backed) and the transactional core for the admission rate (say, 5k checkouts/min). That is a cost decision as much as a technical one. Run a load test that replicates the on-sale (see scale guide §6.1–6.2) before every major event.
🏗️ Flow: the on-sale minute by minute.
T-60m Event page fully cached at CDN; waiting room pre-scaled; bot rules armed
T-15m Users arrive → waiting room (static page, CDN) → device fingerprint, CAPTCHA if suspicious
T-0 Everyone in the "early" pool gets a RANDOM queue position (fairness); late arrivals appended
Admission controller: admit_rate = f(checkout service p99, DB CPU, inventory left)
T+1m Admitted (signed token, 10-min TTL) → seat map (cached snapshot ≤ 1 s old)
Select seats → HOLD (conditional update, all-or-nothing) → 10-min timer
Pay (idempotent) → CONFIRM (conditional on hold_id & not expired) → ticket issued
T+10m Expired holds released → admission controller lets in more users matching freed inventory
Sold out → waiting room switches to "sold out" page (stop admitting; protect everything)
🌍 How it's built in production.
- Ticketmaster, Eras Tour presale (November 2022): Ticketmaster reported about 3.5 billion system requests, 4× its previous peak, largely from bots and fans without presale codes. The site degraded and the public sale was cancelled. Lessons: abusive traffic is part of capacity planning, and verified-fan pre-registration is a business-level admission control.
- Queue-it and similar managed waiting rooms are widely used for drops and on-sales. A good buy answer: the waiting room is commodity; the inventory core is yours.
- Shopify flash sales: Shopify has described using scriptable load balancers (OpenResty/Lua) to queue and throttle checkout traffic for a single shop, so one merchant's drop doesn't flood the pod.
- Alibaba Singles' Day: about 544k orders/s at peak (2019), achieved through months of capacity rehearsal (full-link stress tests in production) and inventory sharding.
⚠️ Corner cases.
- Payment succeeds after the hold expired and the seat was resold. The confirm must be conditional. If it fails, auto-refund immediately and apologize. Never oversell.
- Orphan seats: users leave single empty seats between groups, which hurts sell-through. Enforce adjacency rules at selection time.
- Queue token sharing and reselling: bind admission tokens to the account plus the device, and make them single-use.
- Hold release bursts: thousands of holds expire at the same second (all admitted together), creating a sudden inventory wave. Jitter the hold TTLs.
- Partial hold failure: 3 of 4 requested seats are available. Make holds all-or-nothing in one transaction, and suggest alternatives.
- Price changes mid-flow (dynamic pricing): lock the price on the hold, not at page view.