Guide · System design
Design a Ticket Booking System: Seat Locks and Flash Sales
26 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Ticket booking system design is about one hard rule: never sell the same seat twice, even when lakhs (hundreds of thousands) of fans tap at the same second.
- Claim seats with one conditional update, a single database command that takes a seat only if it is still free. Each seat then has exactly one winner.
- A hold is a short claim on a seat, here 8 minutes, while the fan pays. Book the seats only if the payment arrives before that order's hold ends; otherwise refund it.
- Survive flash sales, when far more fans than seats arrive at once, with a virtual waiting room: a queue page that lets fans in at a fixed rate.
- Beginners can stop after the section on hold timeouts. Engineers then get race tests, many fans grabbing one seat at the same instant, run on the PostgreSQL 16.15 database. They also get why a lock in Redis, an in-memory store, is unsafe alone, a crash-safe payment confirm and the waiting-room maths.
What goes wrong when thousands of fans book the same seats at once?
Two things break at the same time, and good ticket booking system design must handle both. Too many people arrive in the same minute, and code that checks a seat and then books it lets two buyers through.
Picture a common setup. A ticket app in Mumbai sells cinema seats every day, then opens sales for a one-night concert at a 40,000-seat stadium. The setup is made up, but the problems are real.
On a normal day the app handles perhaps 2,000 bookings an hour. For the concert, sales open at 12 noon on a Sunday, and about 6 lakh (600,000) fans are waiting with their phones.
The first big sale goes badly. The next morning, the team lists three problems.
- The app slowed to a crawl at 12:00 and showed errors for 20 minutes. Thousands of fans gave up.
- Eighty-four seats were sold twice. Two fans paid for seat J-14 in the upper stand, and both got a ticket.
- By 12:30, about 3,000 seats were stuck as held by people who had closed the app. Nobody could buy them for an hour.
The double booking comes from a few lines of code. The app read the seat, saw it was free, then wrote it as booked. Two requests read J-14 as free within the same few milliseconds.
This is a race condition: two actions run at the same time, and the result depends on which one finishes first. This guide fixes all three problems on that same concert sale.
How does ticket booking system design work in apps like BookMyShow?
Ticket booking system design splits the work in two. Browsing uses a fast read path that may be a second old, and buying uses a strict write path that sells each seat exactly once.
Start with what the system must do. These are the functional requirements, the features a fan can see and use.
- Browse movies, concerts, venues and show times by city.
- See a seat map that shows which seats are still free.
- Pick seats and hold them for a few minutes while paying.
- Pay through a payment gateway, a company that moves money from the buyer's bank or card to the seller.
- Get a ticket with a QR code by SMS and email.
Then come the non-functional requirements, the rules about how well the system must work. They show where the design must be strict and where it can relax.
- No seat is ever sold twice. This one rule beats speed, cost and everything else.
- Browsing stays fast and available, even when booking is busy during a big sale.
- A held seat that is never paid for goes back on sale within minutes.
- The system survives 6 lakh people arriving in the same minute without falling over.
Notice the split. Browsing can show slightly old data, but booking cannot be wrong even once. That one difference shapes the whole design.
Here are the main parts. A cache is a fast copy of data kept close to readers. A webhook is a message another company's server sends to yours when something happens.
Follow a fan from the app to the seat database. Only the booking service and the hold sweeper ever change seat rows.
The main parts of a ticket booking system
Drag to exploreNotice that reads go to the cache, while every seat change reaches the one seat database through the booking service or the sweeper.
Diagram connections:
- Fan's app or website to Catalog service (Browse shows)
- Catalog service to Seat map cache (Read seat map)
- Fan's app or website to Waiting room (Pick seats)
- Waiting room to Booking service (Admitted fans)
- Booking service to Seat database (PostgreSQL) (Hold and confirm)
- Booking service to Seat map cache (Refresh seat map)
- Fan's app or website to Payment gateway (Fan pays)
- Payment gateway to Booking service (Webhook: paid)
- Hold sweeper to Seat database (PostgreSQL) (Release expired holds)
- Booking service to Ticket sender (Send ticket)
- Hold sweeper to Seat map cache (Refresh seat map)
- Fan's app or website: Browses shows, picks seats, pays
- Catalog service: Movies, venues and show times
- Seat map cache: Copy of seat status, about a second old
- Waiting room: Lets fans in at a fixed rate in big sales
- Booking service: Holds, confirms and releases seats
- Seat database (PostgreSQL): The final word on every seat
- Payment gateway: Takes money, reports back by webhook
- Hold sweeper: Frees seats whose hold ran out, every 30 s
- Ticket sender: Sends the QR ticket by SMS and email
How do you stop two people from booking the same seat?
You make the check and the claim one single step inside the database, so there is no gap between them. The database then lets exactly one request win each seat.
First, the data. Each show gets its own row for every seat, in a table called show_seats. A 40,000-seat concert has 40,000 rows, each with a status: AVAILABLE, HELD or BOOKED.
A hold is a short claim on a seat while the buyer pays. A HELD row also stores booking_id, the order that holds it, and hold_until, the moment the hold ends.
The naive version: check, then write
The first version had two steps, and it is wrong. Step one asked the database whether J-14 was free. Step two, a few milliseconds later, wrote J-14 as booked.
We forced that order on PostgreSQL 16.15, a widely used open-source database, using two database sessions to play two fans. Both reads saw AVAILABLE, both writes saved, and two fans were told yes.
The fix: one conditional UPDATE
The fix replaces both steps with one UPDATE, the SQL command that changes rows. SQL is the language most databases use to read and change data. Its WHERE part matches only seats that are free, or held by someone whose time ran out.
Here conn is a connection from psycopg 3, a common PostgreSQL driver for Python. A transaction groups changes so they are saved together, or undone together if anything fails.
import psycopg
HOLD_MINUTES = 8
HOLD_SQL = """
UPDATE show_seats
SET status = 'HELD', booking_id = %s,
hold_until = now() + make_interval(mins => %s)
WHERE show_id = %s AND seat_id = ANY(%s)
AND (status = 'AVAILABLE'
OR (status = 'HELD' AND hold_until < now()))
"""
class SeatTaken(Exception):
pass
def hold_seats(conn, show_id, seat_ids, booking_id):
"""Hold every seat in seat_ids for booking_id, or none of them."""
seat_ids = sorted(set(seat_ids))
with conn.transaction():
cur = conn.execute(HOLD_SQL, (booking_id, HOLD_MINUTES, show_id, seat_ids))
if cur.rowcount != len(seat_ids):
raise SeatTaken(seat_ids) # the error undoes every seat this call heldThe with block is the transaction. Raising SeatTaken inside it undoes every seat this call changed, so a family of four never ends up holding three seats.
The set call drops a seat listed twice. Without it, two copies of K-1 update one row, the count does not match, and a free seat looks taken. A seat that does not exist also counts as taken.
An isolation level sets how much running transactions see of each other. At Read Committed, PostgreSQL's default, the second UPDATE waits until the first transaction commits or rolls back.
Once the first commits, PostgreSQL tests the second request's WHERE again, against the new row. J-14 is now HELD with time left, so it no longer matches, and the second request changes zero rows.
If the first rolls back instead, the second goes ahead and takes the seat. A plain read of J-14 during the wait returned at once; only the second writer waited.
At the stricter Repeatable Read level, if the first commits, the second request does not re-check. It fails with error 40001, a serialization failure, and your code must retry it.
Check it yourself in psql
You can watch the WHERE check work with psql, PostgreSQL's command-line tool, in a scratch database. Save this as check.sql and run psql -d scratch -f check.sql.
CREATE TABLE show_seats (
show_id bigint,
seat_id text,
status text NOT NULL DEFAULT 'AVAILABLE',
booking_id bigint,
hold_until timestamptz,
PRIMARY KEY (show_id, seat_id)
);
INSERT INTO show_seats (show_id, seat_id) VALUES (1, 'J-14');
-- Fan A asks for J-14
UPDATE show_seats
SET status = 'HELD', booking_id = 77,
hold_until = now() + interval '8 minutes'
WHERE show_id = 1 AND seat_id = 'J-14'
AND (status = 'AVAILABLE'
OR (status = 'HELD' AND hold_until < now()));
-- Fan B asks for the same seat a moment later
UPDATE show_seats
SET status = 'HELD', booking_id = 78,
hold_until = now() + interval '8 minutes'
WHERE show_id = 1 AND seat_id = 'J-14'
AND (status = 'AVAILABLE'
OR (status = 'HELD' AND hold_until < now()));
SELECT seat_id, status, booking_id FROM show_seats;Expect UPDATE 1 for the first fan and UPDATE 0 for the second. The last line shows J-14 as HELD by booking 77. This runs the two requests one after another, so it shows the check, not the race.
Can overlapping groups deadlock?
Yes, when seats are claimed one statement at a time. A deadlock is two transactions each waiting for a row the other holds, so neither can finish.
In our test, two buyers claimed A-1 and A-2 in opposite orders, one seat per statement. All 20 runs deadlocked, and PostgreSQL aborted one transaction each time with error code 40P01.
With one UPDATE per group, 1,400 races between overlapping groups gave no deadlock. The query plan, the steps the database chose to run the query, visited rows in the order they sit on disk, whatever order the list used.
The PostgreSQL 16 locking docs do not say which order one UPDATE locks its rows in (as of 19 October 2026). So catch error 40P01, treat it like a taken seat, and let the fan pick again.
What about locking the rows first?
Another common way is a row lock. SELECT with FOR UPDATE locks the rows it returns until your transaction ends, and other requests for those rows must wait.
This helps when the decision needs code, such as a rule against leaving one empty seat between two groups. The cost is that locks stay held while your code runs.
PostgreSQL adds two options. NOWAIT returns an error at once, code 55P03, instead of waiting. SKIP LOCKED quietly skips rows someone else has locked.
The docs warn that skipping locked rows gives an inconsistent view of the data, fit only for queue-like tables. So use it for jobs like handing out any free seat in a price band.
How long should a seat hold last, and what happens when it ends?
A hold should last a few minutes: long enough to pay, too short to hoard. When it ends, the seat goes back on sale by itself. This design uses 8 minutes for the concert.
Think of a mall car park on the first day of a big sale. A driver at the entrance gets a spot on level 3. The attendant drops an orange cone on it and starts a clock.
If the car arrives in time, the cone comes off and the driver parks. If it does not, the attendant picks up the cone and waves in the next car. Nobody keeps a spot all afternoon by promising to come.
A seat hold works the same way. The cone is the HELD status, and the clock is hold_until. The attendant picking up the cone is the hold expiring.
Where the picture breaks: the attendant can see the car arrive. The server never sees the fan, only a payment message, which can arrive late or twice.
There are two ways to pick up the cone. The first is lazy: the hold UPDATE already treats an expired hold as free. So a stale hold never blocks a fan who asks for that exact seat.
The second is a sweeper, a background job that runs every 30 seconds. It sets expired HELD rows back to AVAILABLE, so the seat map shows them as free again. This fixes the third problem.
UPDATE show_seats
SET status = 'AVAILABLE', booking_id = NULL, hold_until = NULL
WHERE status = 'HELD' AND hold_until < now();The sweeper never touches a BOOKED seat, because its WHERE matches only HELD rows. On one 40,000-seat show with 4,000 expired holds, it ran in 67 ms in our sandbox; your times will differ.
Here is the life of one seat. Start at Available and follow it through the question in the middle.
The life of one seat
Drag to exploreNotice that a seat goes back on sale only through Released, when a hold ran out unpaid. Booked is the end of the line.
Diagram connections:
- Available to Held for 8 minutes (Fan picks it)
- Held for 8 minutes to Paid before the hold ends?
- Paid before the hold ends? to Booked (Yes)
- Paid before the hold ends? to Released (No)
- Released to Available (Back on sale)
- Available: Anyone can pick it
- Held for 8 minutes: Status HELD, with booking_id and hold_until
- Paid before the hold ends?: Checked when the payment webhook arrives
- Booked: Ticket sent, seat never sold again
- Released: Lazy check or the sweeper frees it
Choosing the length is a trade-off. UPI is India's instant bank-to-bank payment system, and some UPI approvals are slow. Too short a hold fails those payments halfway; too long leaves seats idle while thousands wait.
Should you hold seats in Redis or in the database?
Keep the real hold in the database, because that is where the seat is finally sold. Redis can sit in front as a fast filter, but it should not be the final word.
Redis is an in-memory data store, fast because it keeps data in RAM. Its SET command has an NX option, which sets a key only if it does not exist yet. A PX option makes the key expire after that many milliseconds.
So SET seat:J-14 booking-77 NX PX 480000 looks like a perfect cone. On Redis 7.0.15 it succeeded for booking 77, returned nil (no value) for booking 78, and a short test key vanished on time.
You can try the cone in redis-cli, the Redis command-line tool. The first SET prints OK, the same SET for booking-78 prints (nil), and PTTL seat:J-14 prints the milliseconds left, just under 480000.
The catch is subtle. As of October 2026, the Redis SET docs discourage this simple lock pattern in favour of Redlock, a lock spread over several Redis servers. Martin Kleppmann, a distributed systems researcher, argued in 2016 that expiry alone is unsafe when correctness matters.
His example is a paused program. A server holding a lock freezes for a while, perhaps during garbage collection, when the language runtime cleans up memory. The lock expires, someone else takes it, and the first server wakes up and writes anyway.
His fix is a fencing token, a number that grows with every new lock. The storage remembers the highest number it has seen and rejects writes carrying an older one.
In this design the lock and the seat live in the same database. So the confirm step checks ownership, the booking_id, in the same statement that books the seat. That does the job a fencing token does.
Kleppmann also splits locks into two kinds. An efficiency lock only saves duplicate work, so losing it now and then is fine. A correctness lock protects data, and a seat sold twice is exactly that failure.
His analysis is disputed in part. Redis's creator, antirez, published a counterpoint, and the Redis docs link to both. Those docs still tell anyone who cares about correctness to implement fencing tokens.
| Approach | How it works | Good at | Watch out for |
|---|---|---|---|
| Conditional UPDATE | One statement checks and claims the seats | Simple, safe, no long-held locks | Popular seats queue up during a flash sale |
| Row lock with FOR UPDATE | Lock rows first, decide in code, then write | Rules that need code, like no single gaps | Locks held while code runs, and deadlocks |
| Redis SET with NX and PX | One key per seat that expires by itself | Very fast, takes load off the database | Expiry alone cannot stop a paused server writing late |
The final design uses the database for every hold and every confirm. Redis keeps only the cached seat map. If the map shows a seat as free but it is not, the UPDATE changes zero rows, and the app says the seat was just taken.
What happens if a payment succeeds after the seat hold expires?
In this design the booking service refuses the seat and refunds the money. It books only when the payment arrives while the hold still belongs to that same order.
Payments are slow and messy. A fan may approve a UPI request on another phone, or the bank may take a minute. The gateway then tells the booking service through a webhook.
Webhooks can arrive late, twice, or out of order. So the confirm step must be idempotent, which means running it twice has the same effect as running it once.
Check each webhook's signature, a code the gateway computes, usually with a secret only you and it share, the way its docs describe. Otherwise anyone who finds the address can send a fake payment success.
Here is the whole buying flow, one box per message. Each detail line shows who talks to whom.
Buying a ticket, step by step
Drag to exploreNotice the decision box: only the Yes path ends in a ticket. The No path ends in a refund, never in a seat sold twice.
Diagram connections:
- Pick seats to Hold seats for 8 minutes
- Hold seats for 8 minutes to Create payment order
- Create payment order to Fan approves payment
- Fan approves payment to Send payment webhook
- Send payment webhook to Hold still ours, in time?
- Hold still ours, in time? to Mark seats booked (Yes)
- Hold still ours, in time? to Refund the payment (No, hold expired)
- Mark seats booked to Send QR ticket
- Pick seats: Fan's app → Booking service
- Hold seats for 8 minutes: Booking service → Seat database
- Create payment order: Booking service → Payment gateway
- Fan approves payment: Fan's app → Payment gateway
- Send payment webhook: Payment gateway → Booking service
- Hold still ours, in time?: Booking service → Seat database
- Mark seats booked: Booking service → Seat database
- Refund the payment: Booking service → Payment gateway
- Send QR ticket: Booking service → Ticket sender
Here is the confirm step, run on the same test database. The bookings table holds one row per order, with seat_count and a status that starts as PAYMENT_PENDING. The gateway object stands for your gateway's client library.
CONFIRM_SQL = """
UPDATE show_seats SET status = 'BOOKED', hold_until = NULL
WHERE booking_id = %s AND status = 'HELD' AND hold_until > now()
"""
class SeatsGone(Exception):
pass
def on_payment_success(conn, gateway, booking_id, payment_id):
"""Handle a 'payment succeeded' webhook. Safe to run twice."""
with conn.transaction():
row = conn.execute(
"SELECT status, seat_count FROM bookings WHERE id = %s FOR UPDATE",
(booking_id,),
).fetchone()
if row is None:
return "UNKNOWN_ORDER" # log it and check with the gateway
status, seat_count = row
if status != "PAYMENT_PENDING":
return status # a repeat webhook: already handled
try:
with conn.transaction(): # a savepoint inside the outer block
cur = conn.execute(CONFIRM_SQL, (booking_id,))
if cur.rowcount != seat_count:
raise SeatsGone()
status = "CONFIRMED"
except SeatsGone:
status = "REFUND_DUE" # the savepoint undid any half-booked seats
conn.execute(
"UPDATE bookings SET status = %s WHERE id = %s", (status, booking_id)
)
if status == "REFUND_DUE":
gateway.refund(payment_id, idempotency_key=f"refund-{booking_id}")
return statusStart with FOR UPDATE on the order's row. It makes two copies of the same webhook run one after the other. The second copy sees the status already changed, and stops. A webhook for an order you do not know returns UNKNOWN_ORDER for you to log.
Next, the WHERE checks booking_id and hold_until. Say the hold expired and another fan grabbed seat K-2. Its booking_id no longer matches, so the late payment cannot take the seat back.
The inner with block is a savepoint, a marker inside a transaction that you can roll back to. If any seat is missing, only the seat changes are undone, and the order is saved as REFUND_DUE.
The refund call runs after the commit, never inside the transaction. An idempotency key is a unique label the gateway uses to spot a repeated request, so a retried refund goes out once, if your gateway supports one.
One gap remains: a crash between the commit and the refund call. Run a small job that retries the refund for every order still in REFUND_DUE, then marks it REFUNDED.
Two copies of one webhook sent at the same moment, 50 times, booked the seat once with no refund. When a payment arrived just after expiry and a new fan asked at the same moment, 100 times, the fan got the seat and the payer a refund.
This rule refunds even when nobody has taken the seat yet. A gentler rule drops the hold_until check and books whenever booking_id still matches. In one run of 200 races timed right at expiry, it booked the payer 157 times and the new fan 43 times, never both.
Which rule to pick is a product decision: honour the fan who paid a little late, or the countdown they were shown. Either way, extending the hold once by 2 minutes when a fan taps Pay gives slow bank approvals more room. Extend with the same check: only where booking_id still matches and hold_until is still in the future.
How do ticket sites handle flash sales?
They slow the crowd down before it reaches the booking service, using a virtual waiting room that lets buyers in at a fixed rate. The database then sees only as many buyers as it can serve.
A flash sale is a sale where demand far exceeds supply and arrives all at once. The concert has 40,000 seats and 6 lakh fans. That is 15 fans for every seat, so most leave with nothing.
Real sales get far bigger. For the Taylor Swift Eras Tour presale in November 2022, Ticketmaster reported 3.5 billion total system requests, four times its previous peak.
Ticketmaster said over 3.5 million people pre-registered, and about 1.5 million were sent codes to join the sale. It blamed part of the load on bot attacks, and slowed some sales to keep its systems stable.
A virtual waiting room is a holding page in front of the booking flow. Fans who arrive get a place in a queue. The room lets a set number of people in, so the system behind it never drowns.
As of October 2026, Cloudflare's Waiting Room docs describe two main dials. Total active users is a target for how many people may be inside at once. New users per minute is a target for how fast new people may enter. The page describes visitors leaving that queue first in, first out, and each one gets a cookie that manages their turn.
Back at the mall car park, a second attendant stands at the ramp on sale day. She lets a car in only as fast as spots free up. The queue outside is long, but the ramp never jams.
Sizing the dials with round numbers
Say most fans buy about 4 tickets (made up), so 40,000 seats means about 10,000 orders. If 1,000 fans enter each minute and half of them buy, the seats sell out in about 20 minutes.
How many fans are inside at once? Multiply the entry rate by the time each fan stays inside, a rule called Little's law. If a fan stays about 10 minutes on average, picking seats and paying (made up), that is about 10,000.
Set total active users near that number. The seat database then sees a steady 1,000 new fans a minute instead of a noon wall of 6 lakh.
Here is the noon sale from the fan's side. A CDN (content delivery network) is a set of servers near users that keep copies of pages and files.
The noon concert sale
Drag to exploreNotice how each box thins the crowd before it reaches the seat database (numbers made up).
Diagram connections:
- 6 lakh fans at noon to CDN (Open the sale page)
- CDN to Bot check
- Bot check to Waiting room queue (Real fans)
- Waiting room queue to Seats still left?
- Seats still left? to Booking service (Yes, 1,000 a minute)
- Seats still left? to Sold-out page (No)
- Booking service to Seat database (PostgreSQL)
- 6 lakh fans at noon: Phones open, all tapping at once
- CDN: Serves the queue page and seat images
- Bot check: Blocks scripts and repeat accounts
- Waiting room queue: First in, first out, tracked by cookie
- Seats still left?: Checked before letting each batch in
- Booking service: Holds and confirms seats
- Seat database (PostgreSQL): One conditional UPDATE per hold
- Sold-out page: Shown at once to the rest of the queue
How do you stop bots from buying everything?
Bots are scripts that buy tickets faster than any person, often to resell them at higher prices. This design adds three guards. Each account may buy at most 6 tickets, each account needs a verified phone number, and a bot check runs before the queue.
How do you show live seats to thousands of fans?
Serve the seat map from a cache, and accept that it may be a second or two old. The booking service and the sweeper refresh the Redis copy after every hold, booking and release.
Give each cached map a short expiry, a few seconds, so a missed refresh fixes itself. The database still decides: a tap on a seat that just went shows a message and a fresh map.
For huge shows, offer best available. The fan picks a price band and a count, and the system picks the seats. Fewer fans fight over the same front rows, so fewer holds fail.
One more trap is the hot shard. Sharding splits data across database servers, often by show. During the sale, every write lands on the one server holding this concert, so more servers do not spread that load.
How do you explain ticket booking system design in a system design interview?
Lead with the one rule that matters, never sell a seat twice. Then show how holds, payments and a waiting room protect that rule, in this order.
Ask clarifying questions
Numbered seats or standing tickets? How big is the largest show? Is there a flash sale? How long may a fan take to pay?
State the core rule
No double booking, ever, while browsing may be slightly stale. This split justifies a strict write path and a cached read path.
Draw the data model
Shows, show_seats with status, booking_id and hold_until, then bookings with seat_count and status. One row per seat per show.
Show the hold
One conditional UPDATE checks and claims seats together. Explain expiry with a lazy check plus a sweeper.
Walk through payment
Verified webhook, idempotent confirm with a row lock, the booking_id check, and a refund after commit, retried by a job.
Handle the flash sale
Waiting room with an admission rate, bot checks, per-account limits, a cached seat map, and the hot shard.
Do the numbers out loud
Seats, orders, entry rate, time inside and sell-out time, with round numbers. Show that the database load stays steady.
Questions people ask
- What is a seat lock in a ticket booking system?
- A seat lock, often called a hold, is a short claim on a seat while the buyer pays. It stops anyone else from buying that seat for a few minutes. If the buyer does not pay in time, the hold ends and the seat goes back on sale.
- How long should a seat hold timeout be?
- Long enough for a normal payment, and no longer. A few minutes is a common starting point. Measure how long real payments take, including slow bank approvals, and set the timeout a little above that. Extending the hold once when the buyer taps Pay helps slow payments without letting people hoard seats. Extend only a hold that is still yours and has not yet run out.
- Can I use optimistic locking for seat booking?
- Yes. Optimistic locking means you do not lock first. You write only if the row is still in the state you expect, often by checking a version number. The conditional UPDATE in this guide is the same idea, with the seat's status and hold time standing in for the version. Unlike pure optimistic locking, it may wait a moment if another request is changing the same row.
- Which database is best for a ticket booking system?
- Any database that can check and change a row in one atomic step works. Atomic means it happens fully or not at all. Relational databases such as PostgreSQL and MySQL are a common, safe choice because transactions and row locks are built in. Pick what your team runs well.
- What is a virtual waiting room?
- A virtual waiting room is a queue page placed in front of a busy site. It lets visitors in at a controlled rate, usually first in, first out. The site behind it stays responsive, and visitors see their place in line instead of error pages.
- How do you book standing tickets with no seat numbers?
- Use one counter per ticket type instead of one row per seat. The update lowers the count only if enough tickets are left, with a WHERE such as tickets_left >= 4. In our test, taking 4, then 2, then 1 from 5 tickets changed 1, 0 and 1 rows. Each hold records how many tickets it took, and expired holds add them back.
- Why not just add more servers for a flash sale?
- More servers help browsing, but every booking still fights over the same few thousand seat rows. Those rows usually live on one database server, so extra servers cannot share that work. Limiting how fast buyers enter protects the database far better than adding machines.
Sources and further reading
- PostgreSQL 16 documentation: SELECT, the locking clause (FOR UPDATE, NOWAIT, SKIP LOCKED)
- PostgreSQL 16 documentation: transaction isolation (Read Committed re-check, Repeatable Read error)
- PostgreSQL 16 documentation: explicit locking, row locks and deadlocks
- Redis documentation: SET command, NX and PX options and the discouraged lock pattern
- Redis documentation: distributed locks with Redis, consistency disclaimer and analysis links
- How to do distributed locking (Martin Kleppmann, 8 February 2016)
- Cloudflare Waiting Room documentation: how it works
- Ticketmaster: Taylor Swift The Eras Tour onsale explained (19 November 2022)