How do you stop double bookings?
Why checking that a time is free is not enough, the rule that holds when two people book in the same second, and what the one who lost sees.
Checked 6 October 2026 About 3 minutes View as Markdown
The short answer
Two people pick the same time in the same second, and a check in code passes for both. A database rule refusing overlaps is the one check that cannot race.
- One person cannot be booked twice: the database refuses the second.
- Buffers are inside the rule, so nothing is booked into a clean-down.
- An unpaid booking holds its slot for 15 minutes, unless you change it.
Why checking first is not enough
Most booking code does two things in turn: it asks whether the time is free, and if it is, it saves the booking. Between the question and the save there is a gap a few milliseconds long. Two customers who press book at the same moment both ask, both hear that ten o'clock is free, and both save. Neither did anything wrong, and the diary now holds two people at ten.
It happens rarely, which is what makes it hard to catch: never in testing, once on a busy Saturday. Making every booking wait in line behind the others closes the gap, at the cost of slowing exactly the moments that are busiest.
The check that cannot race
Let the database refuse it. Postgres can hold a rule that no two rows for the same person may have overlapping times, called an exclusion constraint, and it applies the rule as it writes, to every write. Two saves arriving together cannot both pass it: one is written and the other is refused, however close together they came.
BackendFree's bookings sit under exactly that rule:
- Per member of staff. Two people can each be booked at ten o'clock. One person cannot be booked twice.
- Over the whole blocked time. A service's buffers are part of the time it takes out of the diary, so nothing can start inside somebody else's clean-down.
- Only while a booking holds its slot. Waiting for payment, waiting for the business, or confirmed. A cancelled or expired booking gives its time back the moment its status changes.
- Touching is not overlapping. One appointment ending at 10:00 and the next starting at 10:00 both fit.
What the customer who lost sees
The second booking is refused with slot_unavailable, the same answer as a
time that was never free, and nothing of it is left half written. Ask for the
free times again and offer the next one.
The time asked for is checked again when the booking is made, rather than trusted from the page that offered it, so a time outside opening hours, inside the notice period or off the grid is refused the same way.
Holds while somebody pays
A booking that has to be paid for holds its slot while the customer pays, 15 minutes unless you change it, and nobody else can take that time meanwhile. A hold that has run out is written off before the next booking for the same time is tried, so nobody is refused a slot that nobody is holding.
If the money arrives after the hold ran out, the booking comes back only if the time is still free, decided by the same rule. If somebody else has taken it since, the booking is marked as owing a refund instead.
A double click is not a second booking
A booking form is often sent twice: a double click, or a retry after a slow
network. Each request to book carries a key of its own, and the same key always
comes back as the same booking. The SDK adds one to every write, so retrying
book() is safe. Booking again to get a new payment page is not: the first
booking is already holding the slot.
Bookings has the rest of the diary, and How do you leave time between appointments? covers the buffers.
Questions people ask next
Can two people on the staff be booked at the same time?
Yes. The rule is per member of staff. Two stylists at ten o'clock are two appointments, and one stylist booked twice at ten is refused.
Can two customers pay for the same slot?
No. A booking waiting for payment holds its slot against everybody else, so the second customer is refused before they are ever sent to pay.
Does a cancelled booking free its time straight away?
Yes. A cancelled or expired booking stops holding its slot the moment its status changes, and the time is offered again.
What does my code have to do?
Handle the refusal. When a booking comes back as slot_unavailable, ask for the free times again and offer the next one. Nothing else has to check.
Sources
- 01Constraints: exclusion constraints, PostgreSQL