How an unpaid hold gets released
Most deposits get paid. Some don’t — and an unpaid hold that never expires is a slot the business can’t sell to anyone. This post is about the safety valve: the scheduled sweep that nudges once before the deadline and, when it passes with no payment, quietly releases the hold so the calendar keeps moving.
Key takeaways
- An unpaid hold that never expired would be a slot the business can’t sell — so the system watches the deadline, not just the payment.
- An EventBridge Scheduler rule runs a sweep on a fixed cadence, looking for PENDING bookings near or past their deposit deadline.
- Before the deadline, it sends one reminder — a single nudge, never a stream of them — and marks the booking reminded so it’s never nudged twice.
- At the deadline, an unpaid hold is released: the slot row is freed and the booking marked RELEASED, so the slot is back on sale within minutes.
- Release is a conditional write — it only frees a booking that’s still PENDING — so a deposit that lands at the last second is never wrongly cancelled.
The holds that go quiet
Most deposits get paid, and Part 3 takes those from PENDING to CONFIRMED within seconds. But some don’t. The customer books, means to pay, and then the meeting overruns or the phone goes in a pocket and the deposit never happens. From the calendar’s point of view this is the dangerous case, because the slot still looks taken — there’s a PENDING hold on it — even though no real commitment was ever made. Left alone, that hold sits on your busiest Saturday table forever, blocking customers who would have paid. The thing that needs acting on here isn’t an event that happened; it’s an event that didn’t — a payment that never came — and a system that only reacts to incoming webhooks would never see it.
Catching an absence needs something that runs on a clock rather than on a trigger. That’s the release sweep: a small scheduled job whose whole purpose is to find holds that are drifting toward their deadline, nudge them once, and free the ones that go past it.
A job on a clock
The sweep is driven by EventBridge Scheduler — a managed cron that invokes a Lambda on a fixed cadence, with no always-on compute and effectively no cost. A sensible cadence is every few minutes. Each run does one simple query: find the PENDING bookings whose deposit deadline is either approaching or already past. Those split into two lanes. A booking whose deadline is close but not yet reached, and which hasn’t already been reminded, gets a single reminder — “your slot’s held until 6pm today, here’s the link” — and is marked reminded so the next run doesn’t nudge it again. One reminder, not a drip-feed, because a customer chased every five minutes cancels out of irritation. A booking whose deadline has passed with the deposit still unpaid goes to the release lane.
Releasing a hold is where the care goes, because it’s the moment a slot goes back on sale and a deposit that arrives a second later must not collide with it. So the release is a conditional write: the booking is moved to RELEASED and its slot freed only if the booking is still PENDING. If the deposit cleared in the gap between the sweep’s query and its write — the customer paying at 5:59 while the 6pm sweep runs — the booking is already CONFIRMED, the condition fails, and the release does nothing. The slot stays sold, the customer keeps their table, and the sweep quietly moves on. And just as in Part 3, the state field means the release is idempotent: a booking is released exactly once, and re-running the sweep never releases it again or frees the same slot twice.
Why a sweep, and not a timer per booking
You could imagine scheduling a one-off release for each individual hold — set a timer for the deadline, fire it, check if the deposit was paid. It works, but it means a scheduled entity per booking to create, track, and tidy up, and a pile of near-simultaneous invocations when a busy evening’s worth of deadlines all fall at 6pm. A single periodic sweep is simpler and cheaper: one rule, one function, one query that scales to whatever the day brings, and idempotent by design because the state field means re-running it never double-releases. The few-minute granularity is plenty — whether a slot frees at 6:00 or 6:04 makes no difference to whether it can be re-sold that evening.
The sweep also carries the system’s guardrails. Its reminder honours quiet hours and the STOP opt-out, so nobody is nudged about a deposit at 2am or after they’ve asked not to be texted. It reminds each booking exactly once and releases it exactly once, so a customer can’t be pestered and a slot can’t be freed twice. And it touches only its own state — it reads PENDING bookings and writes the reminded flag or the RELEASED transition, nothing more — which keeps it safe to run unattended every few minutes, forever. When a hold is released, the freed slot simply reappears in the booking source’s availability, ready for the next customer who’ll actually turn up; and if the business wants to know, a released hold can drop a quiet line into the handover inbox so the front desk can offer that Saturday table to the waiting list.
Design rules that shaped release
- Watch the deadline, not just the payment. An unpaid hold is only visible to a job that runs on a clock; the sweep is that clock.
- A clock, not a timer per booking. One EventBridge Scheduler rule and one query beat a scheduled entity per hold — simpler and cheaper.
- Nudge once. A single reminder before the deadline, marked so the next run never sends another — a chased customer cancels.
- Release on a condition. The hold is freed only if the booking is still PENDING, so a last-second payment is never wrongly cancelled.
- Release once. The state field makes the sweep idempotent — a booking is freed exactly once and a slot never twice.
- The same guardrails apply. Quiet hours and the opt-out list bind the reminder just as they bind every other message.