Engineering
Two bookings, one slot, fifty people trying
The question was whether we needed Docker and Kubernetes to survive 10,000 people booking the same slot. The answer was no, and the reason why is the most useful thing we have learned about databases this year.
- Published
- Jul 12, 2026
- Author
- Bolt 958
- Read
- 7 min
Someone asked us what happens if ten thousand people try to book the same slot at the same instant. The implied answer was containers. It usually is. Scale sounds like an infrastructure word, so the fix must be an infrastructure fix — more pods, more nodes, a service mesh, a diagram with hexagons.
Ten thousand people fighting over one row is not a scaling problem. It is a contention problem. Adding pods does not help you: it adds racers to a race you have not yet decided how to judge.
The naive version, and why it loses
The obvious way to prevent a double booking is to check first. Ask the database whether the slot is free, and if it is, write the booking.
Between the check and the write, there is a gap. It is a small gap, and under real concurrency it is where your product goes to die. Two requests both check, both hear "free", both write. Two humans arrive at the same appointment. One of them travelled.
We have that check. It is still in there. It exists to give people a polite error most of the time, and it is fundamentally incapable of guaranteeing anything. Any check followed by a separate write has this hole. You cannot close it in application code.
The mutex sitting next to it is worse, in the specific way that comfortable things are worse: it works perfectly on one server, so it looks like a solution right up until the day you run two.
Let the database judge
Postgres has a feature for exactly this. An exclusion constraint says: no two rows may exist where these expressions overlap. For us, that means no two non-cancelled bookings for the same host whose time ranges intersect. It is one line of schema, backed by a GiST index.
What it buys you is not a check. It is an impossibility. There is no gap, because the constraint is evaluated by the same machinery that writes the row. Fifty requests race, exactly one row lands, and the other forty-nine get a `23P01` violation, which we translate into a 409 and a sentence that a human can read.
The rule underneath: if two states must never coexist, the guarantee belongs in the database, not in your code. Application code can be raced. A constraint cannot.
We tested it properly
A mock cannot prove a constraint, because the constraint lives in Postgres and a mock is, by definition, not Postgres. So we wrote a script that fires fifty genuinely concurrent bookings at one slot against a real staging database. It refuses to run against production, loudly, because someone will eventually try.
| Attempts | Created (201) | Rejected (409) | Rows in DB |
|---|---|---|---|
| 50 | 1 | 49 | 1 |
One row. Every time. That is not the constraint working nicely, it is the constraint being the only thing that can happen.
The bug the constraint did not catch
Hosts have a grace period — a gap after a session before the next one can start. That check lived in application code, ahead of the mutex, which means it was raceable even on a single server. Two bookings could sit legally end-to-end and still leave the host with no gap between them.
The fix was to stop asking and start declaring. Each booking snapshots its own grace period, and the constraint pads every row by its own value. Same test, second scenario: five out of five double-commits before, zero out of five after.
Adding an interval to a timestamptz is only STABLE, not IMMUTABLE, so Postgres rejects it inside an index expression with a 42P17. Wrapping it in an IMMUTABLE function is safe here only because our grace is whole minutes, which are timezone-invariant. Never do this with days or months — the answer genuinely depends on the timezone.
The deploy trap
The first version of that migration only enforced the rule on rows that had a grace value. New code writes that value. Old code does not.
So in the window between running the migration and deploying the code — a window that is always longer than you think — every new booking was written by old code, arrived without a grace value, and was therefore exempt from the constraint. We had turned double-booking protection off, on production, using a migration whose entire purpose was to strengthen it.
The fix was a `COALESCE` to zero, so old-code rows fall back to the original protection instead of falling out of it. The lesson generalises: a partial index whose condition depends on a column only new code populates is a silent coverage gap, and it opens the moment you run the migration.
Still no Kubernetes. One line of schema, and a test that would notice if it left.
Common questions
How do you prevent double-booking the same time slot?
With a Postgres exclusion constraint (GiST) over the host and the booking time range, rather than a check-then-insert in application code. The constraint makes overlapping rows impossible; a check leaves a gap between reading and writing where two requests can both succeed.
Do you need Kubernetes to handle many users booking the same slot?
No. Same-slot contention is a database concurrency problem, not an orchestration one. More instances add more racers; they do not decide who wins. The database decides.
What is error 23P01?
Postgres exclusion_violation — raised when a write would overlap an existing row under an exclusion constraint. We map it to HTTP 409 and tell the guest the slot just went.
by Bolt 958
Want this for your own bookings?
Set up your page, sync your calendar, and start getting paid at 0% commission.
Become a host