When two customers open the public booking page at the same moment for your last available bike, only one can win. Capacity holds make sure the right one wins — without surprise overbooking — and gives the other a fast, clear "out of stock" message.
The problem holds solve
Without holds, a race condition is easy:
- Customer A loads the page at 9:00:00
- Customer B loads the page at 9:00:01
- Both see "1 bike available"
- Both fill in their details
- Both submit at 9:00:30
Without coordination, both bookings succeed and you've sold the same bike twice.
How holds work
When a customer reaches the "Submit" step on the public page, the
backend creates a 15-minute hold in the reservation_holds table.
The hold contains:
vehicle_model_id,quantity,pickup_at,return_atpickup_location_idaddon_holds(any add-ons being held)session_token— a unique token returned to the customer's browserexpires_at = now() + 15 minutes
From that moment, the availability check subtracts the held quantity from displayed availability. So Customer B, refreshing the page, sees "0 available."
What happens to the hold
Three possible outcomes:
- Customer A finishes and confirms — the create-booking endpoint
consumes the hold (sets
consumed_at = now()) and creates the actual reservation. Customer A wins, Customer B sees "0 available" permanently for that window. - Customer A abandons — they close the tab without finishing. The
hold expires after 15 minutes (
expires_at < now()). Availability re-emerges; Customer B can refresh and try again. - Customer A gets an error and retries — the same session token is preserved across retries, so they don't re-create the hold and double-block themselves.
The 15-minute window
15 minutes is the default expiration. It's a tradeoff:
- Too short (5 min) → frustrated customers who took 6 minutes to fill the form lose their hold
- Too long (60 min) → bikes locked up by abandoned tabs, hurting legitimate bookings
15 minutes balances: typical customer takes 2-5 minutes; abandoned tabs free up within a quarter hour.
The duration isn't currently operator-configurable. Contact support if you want to override.
What customers experience
Holds are invisible to customers. They see:
- Real-time availability that reflects current bookings AND active holds
- A green "0 available" warning if holds + bookings have consumed capacity
- A successful booking confirmation if their hold consummated
They don't see the hold token or the expiration timer; everything is managed server-side.
Operator visibility
Operators can see active holds via the database:
SELECT
rh.id,
rh.vehicle_model_id,
vm.name AS model_name,
rh.quantity,
rh.pickup_at,
rh.return_at,
rh.customer_email,
rh.customer_name,
rh.expires_at
FROM reservation_holds rh
LEFT JOIN vehicle_models vm ON vm.id = rh.vehicle_model_id
WHERE rh.subaccount_id = '<your-subaccount-id>'
AND rh.consumed_at IS NULL
AND rh.expires_at > now()
ORDER BY rh.created_at DESC;
Useful when investigating "why does the public page say no availability?" — abandoned holds might be the answer until they expire.
We don't yet have a dashboard UI for active holds. Likely a fast-follow.
Manually clearing holds
Rare. If you need to release a stuck hold immediately (operator error, testing), delete the row:
DELETE FROM reservation_holds
WHERE id = '<hold-id>';
This frees up the capacity instantly. Don't do this for legitimate customer-in-progress sessions — they'll get a confusing error on submit.
Holds and add-ons
The addon_holds JSON column on reservation_holds tracks which
bookable add-ons (helmets, locks) are being held. The availability
check for add-ons subtracts these too — so a customer in checkout
with 2 helmets pending consumes 2 from the visible add-on availability
of other concurrent customers.
If the customer abandons without consuming, the add-on inventory frees up when the hold expires.
Race conditions still possible
Holds prevent the most common race, but two truly simultaneous attempts within the same millisecond can both succeed if they hit different backend instances. Stripe's idempotency on the actual charge prevents double-billing, but the booking-creation race is theoretically open.
In practice, this is exceptionally rare — and if it happens, you have two reservations for one bike and need to manually cancel one. We're considering a database-level lock on the inventory cap to fully close this gap.
Cleanup
Expired and consumed holds aren't auto-purged today. They accumulate in the database. Run a periodic cleanup:
DELETE FROM reservation_holds
WHERE consumed_at IS NOT NULL OR expires_at < now() - interval '7 days';
Or schedule via your existing maintenance cron. Hold rows are small (few KB each) so this is mostly housekeeping, not a performance concern.
Summary
Capacity holds are why your public booking page reliably shows accurate real-time availability under load. They cost nothing to operate, are invisible to customers, and only briefly tie up inventory during active checkouts.