Most reservation projects fail in the gap between the booking widget and the physical floor. The widget works fine. The problem is that the system thinks a four-top opens at 8:00 while the party that sat at 6:15 is still splitting a dessert, the host has mentally promised that table to a walk-in, and the POS has no idea either of those things happened. That mismatch is where double-books, phantom waitlists, and angry 8:15 guests come from.
This playbook is about closing that gap on purpose, in a defined build order, with the math and the message copy and the integration contracts spelled out. It assumes you already know what an SLA and a turn time are, and that you have engineering or product resources — even one capable person — to actually ship this.
If your underlying seating-cadence logic isn't nailed down yet, read Increase table turnover without hurting service: time‑budget templates and seating cadence by service style first. This playbook builds the system on top of that cadence thinking rather than re-deriving it.
The one problem this solves: the booking layer and the floor layer disagree
Everything downstream of reservation and seating workflows traces back to a single failure: two systems hold state about the same table and never reconcile in real time.
The booking layer — your reservation provider, the website widget, the phone — assigns time slots against a theoretical turn time. The floor layer (the host stand, the POS, the actual humans) runs on actual turn time, which is never the theoretical one. Nobody writes the delta back anywhere.
A typical example: a 90-seat bistro sets a flat 90-minute turn for all parties. On a Friday, two-tops are actually running 68 minutes and six-tops are running 118. The system overbooks the small tables (holding them 22 minutes too long) and double-books the big ones (assuming they free up 28 minutes before they actually do). The host papers over both all night with gut feel. Some nights it works. The nights it doesn't, you get a 40-minute wait with visibly empty tables — which is the single most corrosive thing a guest can see.
The fix isn't a better widget. It's a feedback loop: measure real turn time, feed it back into pacing, and cap how many parties you seat per 15-minute window so the kitchen and floor never absorb a wall of simultaneous sits.
Phase roadmap: ship it in four sprints, not one big launch
Trying to deliver reservations, seating, POS integration, and overbooking in one release is how projects die in QA. Sequence it so each sprint is independently useful even if the next one slips.
Eliminate operational bottlenecks effortlessly.
Dineoly helps you manage every reservation, order, and staff shift seamlessly.
- Unified reservation and order management
- Real-time staff scheduling
- Inventory and sales tracking
No credit card required
| Sprint | Deliverable | What "done" means | Why this order |
|---|---|---|---|
| 1 | Real turn-time measurement | Every table has a logged seat-time and clear-time; dashboard shows actual turn by party size | You can't pace what you can't measure |
| 2 | Per-15-min pacing caps + dynamic turn | Booking slots respect caps; turn time is party-size-specific, not flat | Stops the wall-of-sits and the small-table overbook |
| 3 | SMS confirm/cancel cadence | Automated confirm, reminder, cancel-release flow live | Recovers no-show inventory, feeds cancellations back to availability |
| 4 | POS↔reservations integration + safe overbook | POS fires seat/clear events; overbook math runs on real no-show rate | The whole loop closes; state reconciles automatically |
The mistake teams make is starting at Sprint 4 because it's the most exciting. If you integrate the POS before you're measuring real turn times, you just pipe garbage faster. Build the measurement first, even if it feels unglamorous.
Sprint 1 in detail: measuring real turn time
You need two timestamps per table per party: seatedat and clearedat. If your POS can emit a "check closed" event, that's your cleared_at proxy — though it runs slightly early since people linger after the check drops. If it can't, the host marks clear on the stand. Less reliable, but workable.
Turn time for a party = clearedat − seatedat.
SELECT partysize, COUNT(*) AS coverscount, ROUND(AVG(EXTRACT(EPOCH FROM (clearedat - seatedat)) / 60)) AS avgturnmin, ROUND(PERCENTILECONT(0.85) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (clearedat - seatedat)) / 60)) AS p85turnmin FROM seatings WHERE seatedat >= NOW() - INTERVAL '60 days' AND clearedat IS NOT NULL AND serviceperiod = 'dinner' GROUP BY partysize ORDER BY partysize;
Use the p85, not the average, for pacing math. The average lies because it's dragged down by the quick turns. If you pace on the average, roughly 15% of your tables blow past the slot you booked against — and those are the ones that cause the visible waits. Pacing on p85 means you're right most of the time and the overruns are the exception.
One thing worth flagging: turn times aren't stable across the night. The 5:30 covers eat faster than the 8:00 covers, who are there for the evening. Segment your p85 by hour band (early / prime / late) if the data supports it. Most 60-day windows have enough volume to split into at least two bands.
The formulas you actually need
Dynamic turn time
Flat turn time is the original sin. Replace it with a lookup keyed on party size and hour band, built from the Sprint 1 query:
dynamicturn(partysize, hourband) = p85turnmin[partysize][hour_band] + buffer
The buffer is reset and clean time — the gap between cleared and the next party seated. Most rooms run 5–8 minutes. Don't skip it; a zero buffer is why the next reservation stands at the host stand while a busser is still wiping the table.
A worked example for a mid-size room:
| Party size | Prime p85 turn | Buffer | Dynamic turn booked |
|---|---|---|---|
| 2 | 72 min | 6 | 78 min |
| 4 | 94 min | 7 | 101 min |
| 6 | 118 min | 8 | 126 min |
Compare that to a flat 90 across the board. Two-tops were being held 12 minutes too long — lost inventory. Six-tops were double-booked by 36 minutes, which is a guaranteed wait. Fixing just this, before any automation, usually recovers a handful of covers a night off the small tables alone.
Safe overbook
Overbooking is only safe when it's bounded by your measured no-show rate and a hard cap on simultaneous risk:
safeoverbookslots(window) = floor( expectednoshows(window) × confidencefactor ) expectednoshows(window) = bookedparties(window) × noshowrate(segment)
The confidence_factor should sit below 1 — somewhere around 0.6–0.7 — because you never want to overbook against your full expected no-show count. The variance will burn you. If your Friday prime no-show rate is 11% on 40 booked parties, expected no-shows are roughly 4.4, and a 0.65 confidence factor gives you about 2 safe overbook slots for that window. Not four. Two.
The mistake here is overbooking on a blended, all-week no-show rate. No-show rate is wildly segment-dependent: deposit-holding parties no-show at a fraction of the rate of free online bookings. Pull noshowrate per segment or you'll overbook your most reliable guests and strand them at the door.
Per-15-minute pacing caps
This is the lever that protects the kitchen and the host. Cap how many new covers can be seated in any rolling 15-minute window:
pacingcap15 = min( kitchenticketsper15capacity, hostseatthroughputper15, floor(availablecoversnext_15) )
The kitchen number is usually the binding constraint. If your line can fire 45 entrées per 15 minutes cleanly and your average check is 1.8 entrées per cover, your kitchen pacing cap is roughly 25 covers per 15. Book past that and every table sat in that window gets slow food — which ironically lowers turnover because frustrated tables linger and complain.
Host dashboard: what goes on the screen
The host stand doesn't need your analytics suite. It needs five things visible at a glance, refreshed every 30–60 seconds.
Here is a text wireframe of the stand view:
┌─────────────────────────────────────────────┐ │ NOW 7:42pm PACING: 22/25 next 15 ⚠ NEAR │ ├─────────────────────────────────────────────┤ │ COMING DUE (tables clearing soon) │ │ T12 (4) seated 6:21 → due ~8:02 [92m] │ │ T07 (2) seated 6:38 → due ~7:56 [78m] │ │ T21 (6) seated 6:05 → due ~8:11 OVER 6m 🔴│ ├─────────────────────────────────────────────┤ │ ARRIVING │ │ 7:45 Patel (4) CONFIRMED ✓ │ │ 7:45 Nguyen (2) NO REPLY ⚠ │ │ 8:00 Walk-in hold (2) │ ├─────────────────────────────────────────────┤ │ WAITLIST: 2 parties est 15–20m │ └─────────────────────────────────────────────┘
The two most important fields: pacing against cap (top right) and the OVER flag on tables past their dynamic turn. The pacing meter tells the host when to slow-walk the next seat. The OVER flag tells them which table to go check on — not to rush the guest, but to understand reality so they stop promising that table to someone on the waitlist.
SELECT COUNT(*) AS coversseatedlast15 FROM seatings WHERE seatedat >= NOW() - INTERVAL '15 minutes';
Compare that live against pacingcap15. When it hits 90% of cap, the dashboard flips to NEAR and the host knows the next party waits a beat.
Show the host only the top three imminent actions (pacing, OVER tables, and next arrivals) to reduce cognitive load and speed decisions.
The dashboard itself doesn't need to be complex — the signal value comes from having these numbers visible and current, not from how polished the interface looks.
SMS, confirmation and cancellation: copy and cadence
The messaging exists to recover inventory, not to nag. Every message should make it easy to cancel — because a cancellation three hours out is recovered inventory, and a no-show is dead inventory.
-
Booking confirmation — immediate, on reservation creation.
-
Reminder + confirm ask — T-24 hours for prime/weekend, T-4 hours for same-day bookings.
-
Final hold notice — T-2 hours only for large parties or deposit bookings.
-
Cancellation release — instant, when guest cancels, pushing the slot back to availability.
> Confirmation: "You're booked! [Restaurant] — Fri 7:45pm, party of 4. Reply C to confirm, X to cancel. We hold the table 15 min past reservation time."
> T-24 reminder: "Looking forward to seeing you tomorrow at 7:45pm (party of 4). Reply C to confirm or X if plans changed — releasing the table helps the next guest. Thanks!"
> Cancellation ack: "Got it, your 7:45pm is cancelled — no charge. Hope to see you another time."
The "releasing the table helps the next guest" line matters more than it looks. That pro-social framing measurably lifts the cancel-ahead rate versus a cold "reply to cancel." People who feel like cancelling is the considerate thing to do actually do it, instead of just ghosting.
When a cancellation fires, the released slot should immediately re-enter the pacing math and, if there's a waitlist, trigger a host prompt — not an auto-text to the waitlist. Auto-texting the top of the waitlist a confirmable slot sounds efficient until two people both confirm the same table. Keep a human in that loop.
POS ↔ reservations integration spec
This is the contract that makes state reconcile. Keep the event set small and the SLAs tight.
| Event | Payload (core fields) | SLA | Consumer action |
|---|---|---|---|
table.seated | tableid, partysize, reservationid?, seatedat | < 5s | Start turn clock, decrement pacing window |
check.opened | tableid, checkid, opened_at | < 5s | Confirm seat is real (reconcile vs host stand) |
check.closed | tableid, checkid, closed_at, total | < 5s | Flag table as clearing, update real turn-time data |
table.cleared | tableid, clearedat | < 10s | Free table, release to availability |
Events reservations sends to POS/host system:
| Event | Payload | SLA | Purpose |
|---|---|---|---|
reservation.assigned | reservationid, tableid, time | < 5s | Pre-assign table so seated event can match |
reservation.cancelled | reservationid, releasedat | < 5s | Return slot to availability + pacing |
The reconciliation rule that prevents double-books: a table cannot be assigned to a new reservation while it has an open check and no table.cleared. That single guard, enforced server-side, eliminates the most common double-book — assigning a "due soon" table that hasn't actually turned.
Idempotency: every event carries an event_id; consumers dedupe on it. POS webhooks retry, and without dedupe you'll double-decrement your pacing window and start refusing seats you actually have.
How the event flow ties together across a full service cycle — from reservation assigned through table cleared — is worth mapping before you write the integration code. Below is a simplified view of that sequence:
[Reservation Assigned] ↓ [table.seated fired by POS] ↓ [Turn clock starts / pacing window decrements] ↓ [check.opened → seat confirmed] ↓ [check.closed → table flagging as clearing] ↓ [table.cleared → availability released] ↓ [Pacing math updates / waitlist host prompt if applicable]
Walking through this once with your engineering team before Sprint 4 surfaces most of the edge cases — the missed cleared event, the retry storm on check.closed, the reservation that was assigned but the party never showed. Better to find those on a whiteboard than in production on a Saturday night.
Operational failover: what the floor does when the system is down
Integration will drop mid-service. The failover plan has to exist before launch, or the host freezes.
-
If POS events stop flowing dashboard shows a STALE banner after 2 minutes of no events. Host switches to manual clear-marking on the stand. Turn clocks keep running off last-known seat times.
-
If the booking system is unreachable host runs the paper/whiteboard grid for the window, caps seating at the conservative flat pacing number (use the low estimate), reconciles when it's back.
-
If SMS provider fails confirmations queue and send on recovery; host calls large-party and deposit bookings manually for the affected window only.
The rule is degrade gracefully, never hard-stop. A host who can't seat anyone because the screen is blank is worse than no system at all. Every automated step needs a manual fallback written on a laminated card at the stand.
Policy templates: timed seating and deposits
Policy is where you decide how aggressive your overbooking and turnover math can be without it reading as hostile to guests.
Timed seating (soft): "Tables are reserved for [dynamic_turn] minutes. If you'd like to linger, just let us know and we'll do our best." This is the right default for most rooms — sets the expectation without a stopwatch.
Timed seating (hard): only for high-demand prime slots or communal tables. State it at booking and at seating. Never spring it on a seated guest.
Deposits: reserve for parties above a size threshold (commonly 6+) and peak dates. A per-head deposit — say $10–20 per head, redeemable against the check — cuts large-party no-shows dramatically. Deposit bookings become your near-zero-no-show segment, which is the single highest-leverage input to your safe-overbook math.
Who should not run hard timed seating or deposits: a neighborhood spot that isn't demand-constrained. If you're not regularly turning guests away, these policies create friction without any real upside. Deposits make sense when demand exceeds supply, not before.
Privacy and compliance notes
You're collecting phone numbers, booking history, and behavioral data (no-show flags). A few guardrails worth keeping:
-
SMS consent get explicit opt-in at booking for transactional and any marketing texts, and keep them separate. Transactional confirmations are generally fine; marketing blasts need their own consent and an easy STOP.
-
Data retention don't keep guest phone numbers indefinitely. Set a retention window (e.g., 24 months of inactivity) and purge.
-
No-show flags are sensitive a "no-show history" field is operationally useful, but don't expose it on a screen guests can see, and be thoughtful about using it to deny service.
-
Deposit/payment data lives with your payment processor, not your reservation DB. Store a token, never raw card data.
Be explicit about consent and retention in your privacy policy and operational runbooks so hosts and managers follow the same rules.
A/B testing plan: prove the pacing caps actually work
Don't roll dynamic turn and pacing caps to the whole book at once. Test it.
-
Split by night, not by table. Run dynamic-turn pacing on Tue/Thu, keep the flat-turn system on Wed/Fri as controls, alternate weekly to wash out day-of-week effects.
-
Primary metric
covers seated per service, measured against wait complaints and average seated-to-entrée time as guardrails. More covers is only a win if service quality holds.
-
Secondary metric
empty-table-during-wait incidents (the thing guests hate most). This should drop toward zero under proper pacing.
-
Run for at least 3–4 weeks so you have enough comparable nights. One good night proves nothing.
-
Kill criteria
if seated-to-entrée time climbs past your service standard, your pacing cap is too high — lower it before chasing more covers.
The honest expectation: dynamic turn plus honest pacing usually recovers a modest, steady lift — a few extra covers a night off better small-table rotation and fewer phantom holds — without the chaos. It is not a 20% overnight jump. Anyone promising that is selling you the overbook variance that blows up on a busy Saturday.
Real scenario
A 74-seat neighborhood Italian spot ran a flat 90-minute turn and booked straight off it. On weekends they'd routinely show a 25–35 minute quoted wait with two or three empty four-tops the guests could see — because the system thought those tables were still "due."
They shipped Sprints 1–2 only: real turn measurement, then dynamic turn (two-tops came in around 74 minutes, six-tops around 120) plus a 24-cover per-15-min pacing cap tied to the line's capacity. No POS integration yet — hosts marked clears manually.
Over the following six weeks the visible-empty-table-during-wait problem mostly disappeared, and they picked up roughly 6–9 extra covers on a typical Friday and Saturday purely from rotating the small tables on their real turn time instead of the inflated flat number. Quoted waits got shorter and more accurate. The kitchen stopped getting slammed by six simultaneous sits at 8:00 because the pacing cap smoothed arrivals. POS events got added about a month later; the manual clear-marking had already proven the model worked.
The win came from measuring reality and respecting the line's throughput — not from the integration. The integration just made it automatic.
Where to start Monday
If you do nothing else this week, start logging seatedat and clearedat for every table and pull your real turn time by party size.
Almost every double-book and phantom wait traces back to a flat turn number that stopped being true months ago. The pacing caps, the overbook math, the POS events, the SMS cadence — all of it is just ways of acting on that one measured truth instead of a guess.
Build the measurement, pace on the p85, and only then automate the loop. A reservation and seating system that knows what the floor is actually doing is worth ten that only know what they hoped it would do.
Ready to elevate your restaurant operations?
Join 2,000+ restaurants using Dineoly to enhance efficiency, increase table turnover, and delight diners.