Double-booked by the booking agent: where the slot lock went missing
In short
Double booking happens when a channel offers a time it has not reserved. Availability may be read from a cache; a commitment never may. Between showing a patient a slot and confirming it, something has to hold that slot exclusively, with an expiry — and the write that confirms it has to fail loudly if the slot moved while the caller was talking.
Key takeaways
- Availability is a cacheable read. A booking is a write that must be conditional on the slot still being free at the moment it lands.
- A hold with no expiry becomes a permanent block; a hold with no owner cannot be released when a call drops mid-sentence.
- If the practice system enforces no conflict constraint of its own, no amount of care in one channel prevents collisions from another.
- Paper and verbal bookings at the desk are a real channel, and they are the one channel a hold cannot reach.
- Reproducing the collision deliberately, with 2 channels inside the cache window, takes an afternoon and settles the argument.
Before reading a single log, reproduce it. Have 2 channels request availability for the same provider at the same moment, then have both commit the same slot a few seconds apart, and record 5 timestamps: when each channel read availability, when each attempted to write, and when the practice system accepted each write. The gap between the read and the write is where double bookings live, and the size of that gap tells you whether you are looking at a caching problem, a missing hold, or a scheduling system that will accept 2 appointments in one slot without complaint.
The underlying rule is short enough to put on a wall. Availability may be cached; a commitment never may. Everything a booking surface shows a patient is a read of a fast-moving list, and reads can safely be seconds stale. The moment the patient says yes, the system has to stop working from its own copy and go back to the source, conditionally, in a way that fails if the world moved. Agents make this visible because they hold a caller in conversation for 40 to 90 seconds between offering a time and confirming it — a window a web form closes in 3.
Force the collision on purpose, then read the 5 timestamps
- Pick one provider and one afternoon in a test environment, or a genuinely empty session in production if there is no test copy that shares the same scheduling rules.
- Read availability from both channels within a second of each other and record both responses in full, including whatever version or revision marker the response carries.
- Commit from channel A and let it succeed. Do not refresh channel B.
- Commit the same slot from channel B roughly 30 seconds later, still inside the window where its cached list says the slot is free.
- Record what happened: an error naming the conflict, a silent success producing 2 appointments, or a success that overwrote the first booking. Those 3 outcomes have 3 different owners.
- Repeat with the desk as channel B, booking directly in the practice system while an agent call is in progress. This is the case teams forget, and it is the one that cannot be fixed by any change to the agent.
The reservation that has to exist between the offer and the yes
A hold is a real object, not a flag. It names the slot, the channel and the conversation that owns it, it carries a creation time and an expiry, and it makes the slot unavailable to every other reader for as long as it lives. Three properties decide whether it helps or hurts.
- It expires without being told to. Calls drop, patients hang up mid-sentence, browsers close. A hold that survives its conversation is a slot removed from the day by a caller who never booked. Pick an expiry from the real conversation length — 2 to 3 minutes covers a phone exchange with a slot offer and a confirmation, and anything above 10 minutes will quietly empty an afternoon.
- It is owned, so it can be released early. When the caller picks a different time, or the agent escalates to a human, the hold on the rejected slot has to be dropped immediately rather than left to time out.
- It converts, rather than being replaced. The commit turns the hold into an appointment against the same identifier. Where the commit writes a fresh appointment and separately deletes the hold, there is a window between the 2 operations in which the slot is free — which is the exact defect the hold was introduced to remove.
The commit itself has to be conditional. Send the version or revision marker the slot carried when it was read, and require the practice system to reject the write if that marker has changed. Where the interface offers no such marker, the fallback is to re-read the slot inside the same transaction and refuse to write if it is no longer free — weaker, because it narrows the race rather than closing it, but far better than writing blind. A booking that succeeds unconditionally is not a booking; it is an assertion.
A slot a patient has been offered but not yet accepted is neither free nor booked. If your data model has no third state, the collision is not a bug — it is the model working as designed.
5 ways 2 channels end up writing into one slot
| Cause | Timestamp signature | Who can fix it |
|---|---|---|
| Availability served from a stale cache | Both reads predate the first write; the cache age exceeds the conversation length | You — shorten the cache, or bypass it once the patient chooses |
| No hold between offer and confirmation | Reads are fresh, writes are seconds apart, both succeed | You — introduce a hold with an expiry |
| Practice system enforces no conflict constraint | Two appointments exist in one slot with no error anywhere in the logs | The scheduling system, or an operating rule plus a detector |
| Sync lag between an external booking surface and the practice system | Write lands promptly on the surface, arrives minutes later in the practice system | Shared — reduce the lag, and never treat the surface as the source of truth |
| Template changed while the call was in progress | A slot that existed at read time is gone or reshaped at write time | Operational — restrict template edits, or re-validate against the template on commit |
The template row is the one that surprises people. Sessions get shortened, a provider is pulled to cover elsewhere, an urgent hold is released into the bookable pool at 08:00 — and each of those rewrites the shape of the day underneath conversations already in flight. The objects involved and the rules that govern them are set out in how a provider template encodes a clinic day; a booking channel that does not understand blocks, holds and release rules will offer times that were never bookable in the first place.
Separating what you can fix from what the scheduling system owns
This split is the whole point of the reproduction exercise, because the 2 halves have completely different economics. Caching, holds, conditional writes and idempotency on retries sit inside the software you are building, and are a week or 2 of careful work. A practice system that will happily store 2 appointments in one slot is a constraint you inherit, and the honest response is a detector rather than a promise.
- Make every commit idempotent. A retried write after a network timeout must not create a second appointment — key it on the conversation, not on the attempt.
- Re-validate at commit against the live template, not against the availability you cached at the start of the call.
- Run a conflict sweep on a schedule, hourly during clinic hours, that lists any slot holding more than 1 appointment and puts it in a queue with an owner. This is the safety net for the case you cannot prevent, and it is worth building even when the constraint exists.
- Resolve conflicts by rule, not by whoever notices first. Earliest committed booking keeps the slot; the later one is contacted with alternatives before the day arrives, never on the morning.
- Log every offer, hold, expiry and commit with its channel. Without that, the next argument about double bookings is another round of anecdotes.
The distinction between a provisional statement and a committed one runs through clinical software generally, and the discipline is the same wherever it appears: what a system publishes provisionally and what it stands behind are different objects with different obligations, as in what a final and a preliminary report each commit to. A slot offer is provisional. An appointment is not.
What a correct hold still will not prevent
A hold makes the slot exclusive. It says nothing about whether the booking should have been made. An agent can hold, commit and confirm an appointment that is the wrong duration, with the wrong provider, for a patient the clinic cannot yet identify — and none of that shows up as a conflict. The fields that have to exist before an appointment genuinely counts as booked are set out in what a registration must carry before a first visit is real, and collecting them mid-call is precisely what stretches the hold window. Pre-filling from records the patient has already given someone else shortens that window materially, which is a concurrency benefit as much as a convenience one — the method is in pre-filling intake from records.
Nor does it cover work arriving through a queue rather than a conversation. Referrals get booked in batches by staff working a list, often against slots held back for exactly that purpose, and a batch process that reads availability once and writes 30 appointments will collide with every live call in progress — one more reason the state of that queue has to be explicit, as argued in referrals that arrive with nobody able to tell which are unbooked.
Most of this work is unglamorous plumbing between systems that were never designed to share a calendar, which is the kind of build we scope as internal tools and operations. It sits alongside the rest of front desk, intake and patient access in a healthcare operations programme, and it is worth settling before a booking agent goes anywhere near a real clinic day.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
Why does a booking agent offer slots that are already taken?
Because it is reading a cached list of availability and committing without re-checking. Caching the read is correct — availability is queried constantly and changes rarely within a few seconds. The defect is committing against that cached view: between the moment the agent offered the time and the moment the caller agreed, another channel booked it, and the write went through because nothing checked.
How long should a slot hold last?
Long enough for the conversation and no longer — 2 to 3 minutes suits a phone exchange that offers a time and confirms it. Measure your own calls rather than guessing, take the point by which most bookings are complete, and add a small margin. Holds that live for 10 or 15 minutes silently remove capacity from the day, because every abandoned call leaves one behind.
Our practice system allows 2 appointments in one slot. What now?
Accept that prevention is voluntary and build a detector. Every channel you control should hold and commit conditionally, but a system with no constraint of its own cannot stop the desk booking over a slot by hand. Run an hourly conflict sweep during clinic hours, route every collision to a named owner, and resolve by a published rule — earliest commit keeps the slot, the later patient is contacted with alternatives before the day.
Does deliberate overbooking make this harder to detect?
Only if the 2 are not modelled separately. An intentional double book is a scheduling policy applied to a named session, and it should be recorded as such — a slot with a permitted capacity greater than 1. An accidental collision is a slot with a capacity of 1 holding 2 appointments. Once capacity is an explicit field, the conflict sweep reports only genuine defects.
- scheduling
- booking agents
- front desk
- concurrency
The work behind this page
Builds from our portfolio that this page draws on.
Read next
- The provider schedule template: slots, blocks, holds and release rulesA template is 4 objects and the rules connecting them. A booking system that cannot read all 4 looks either too eager or too restrictive, and both cost real capacity.definition
- Referrals come in by fax, email and portal, and none of them get bookedReferrals do not get lost because staff are careless. They get lost because a referral has no state, and something with no state has no age and no owner.diagnostic
- Reminders are sending and no-shows are flat: finding the step that failsA reminder can only fix forgetting. Join send logs to attendance, test 4 gates in order, and the flat number usually turns out to be a problem no wording change reaches.diagnostic
- Eligibility returns 'patient not found' for a clearly covered patientNot found is a statement about matching, not about coverage. Re-run the lookup with fewer identifiers, find which field broke the match, and stop retyping the member number.diagnostic
- Recall, waitlist and hold: three different queues everyone calls one thingRecall fires on a clinical due date, waitlist fires on a cancellation, hold waits for the patient. Merge them and every automated message is wrong for two of the three.definition
- Visit type: the one field that decides duration, room, provider and prepVisit type is not a label on a calendar entry. It is the key every scheduling rule joins to, and where it is unmodelled no booking automation has anything to reason with.definition
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation