Rate limits and the sale-day gate
After this you can answer: How does MiniTickets survive a sale day without overselling or falling over?
Picture it
Section titled “Picture it”flowchart TB R["booking request"] --> L{"token left?"} L -->|"no"| E429["429, Retry-After"] L -->|"yes"| G{"seats reserved in Valkey?"} G -->|"no"| E409["409 sold out"] G -->|"yes"| PG["Postgres transaction<br/>final authority"]How it works
Section titled “How it works”- Token bucket. Each caller has a bucket. Booking: up to 10 tokens, one new token per second.
Log-in: up to 20 per IP, one new token every 3 s. No token: 429 with a
Retry-Aftertime. - Atomic. One Lua script in Valkey reads and takes the token in one step, so all api tasks share one count.
- The gate. Another Lua script tries to reserve the seats in Valkey. Denied means sold out: 409, without touching Postgres.
- Postgres decides. A reserved request still runs the locked transaction. If Valkey fails, the gate is skipped and Postgres decides alone; a loop repairs the counter later.
In MiniTickets
Section titled “In MiniTickets”The limiter is apps/api/src/services/rate-limits.ts → enforceRateLimit,
the gate apps/api/src/services/booking-inventory.ts → createGatedBooking
(on by default, INVENTORY_GATE_ENABLED).
The two fail in opposite directions on purpose. The limiter fails closed: without Valkey it answers 503, so expensive password checks never run unbounded. The gate fails open: without Valkey it steps aside, because Postgres can still decide correctly.
Measured locally (128 requests, 32 users, 16 seats): with the gate on, 16 booking transactions reached Postgres instead of 128 (87.5% fewer), throughput rose from 784 to 1107 requests per second, and 0 seats were oversold in both runs.
| From → to | What happens | What allows it |
|---|---|---|
| API app → Inventory gate | a booking asks the gate before the database transaction |
|
| Inventory gate → ElastiCache Valkeyonly when dataEnabled = true | seat counters and leases per event |
|
See it in AWS
Section titled “See it in AWS”The buckets and counters are keys in Valkey. 429s show in the API log; the ALB counts all responses by status.
aws logs tail /minitickets/dev/api --since 10m --filter-pattern 429
Break it
Section titled “Break it”| Symptom | Cause | Where to look |
|---|---|---|
| Real users get 429 | Many users share one office IP for log-in | 429s per client IP |
| Log-in answers 503 for everyone | Valkey unreachable; the limiter fails closed | redis-sg; Valkey status |
| Sold out, but Postgres shows seats | The Valkey counter drifted | “inventory reconciliation” log lines |
Say it in an interview
Section titled “Say it in an interview”Follow-ups
Section titled “Follow-ups”Why a token bucket and not a fixed window?
A fixed window allows a double burst at the window edge. A bucket allows a short burst, then a steady rate, which matches how real people click.
Why is the client IP tricky behind a load balancer?
Every request arrives from the ALB. The real IP is in X-Forwarded-For. TRUST_PROXY_HOPS says how
many proxies to trust; too many, and a client can write its own IP into the header.
What would you add for a really big sale?
A waiting room in front, seat holds that expire, and more api tasks with autoscaling. They are explained in the decisions, not built.
Advanced: Reconciliation
Each api task runs a repair loop every 5 seconds over at most 50 events per batch. It compares the Valkey counter with Postgres and fixes drift, for example after a crash between reserving and settling. The 5 s is a scheduling interval, not a repair promise: a large backlog takes longer.
Checkpoint
Section titled “Checkpoint”Answer these without scrolling up. If one feels hard, reread that section.
- What are the booking and log-in bucket sizes and refill rates?
- Why must taking a token be atomic, and how is it?
- What does the gate save Postgres from?
- Which check fails closed and which fails open, and why?
- Who makes the final booking decision, and why?
Redraw it from memory: request → token left? → seats reserved? → Postgres, with the 429 and 409 exits
Try this: on the map, play the sale-day flow and open the failure branch of the inventory gate step
Read this book online: https://book.theversehub.in/