How it’s built

Why every hold and every sandbox has an end

A place in a pickup slot and a sandbox to play in are both borrowed. This is how each loan says when it ends, which object enforces that, what still ends a sandbox when the nightly wipe fails, and the one case where the end arrives after the money.

A place nobody paid for

A visitor orders two cardamom buns for the 10:15 pickup, reaches the checkout and closes the tab. The day’s Schedule object has already counted the order against the slot. Pickup slots take 6 orders by default (docs/domain.md, as built on 2026-11-05), so five more visitors can have 10:15 and the seventh is told slot-full, though the first order will never be paid. A sandbox has the same shape: a visitor who does not come back leaves tens of objects behind, about 2,500 rows written to seed them (docs/adr/0005and docs/cost.md, estimates as of 2026-11-04), and they are billed until something deletes them.

What a simpler design does, and where it breaks

The usual first design is a cleaner: a job that scans for holds and sandboxes that look abandoned and removes them. It needs a definition of abandoned, and every definition races with a visitor who is slow rather than gone. It is also one more thing that has to be running. If a run is missed, nothing ends, and nobody sees it until the slot is full of unpaid orders. No lab page shows this one yet. The lab’s index is where the lab explains the family of techniques around it; as read on 2026-11-06 it has five labs and none on expiry.

A promise with its end written down

The older answer is the lease. Gray and Cheriton’s 1989 paper on file-cache consistency grants a client rights for a fixed term, so that a server never has to find out whether the client is alive: when the term runs out, the rights are the server’s again. The bakery uses the idea more broadly than the paper. What matters is the order of events. The end is decided when the loan is made, so nothing ever has to discover that something was abandoned, and the object that granted the loan is the one that takes it back.

How the bakery writes the end down

A hold. Placing an order sends the Schedule a Hold whose expires_at is 15 minutes of business time after placement; the Order’s held_until is the same instant. In the transaction that writes the hold, the Schedule (workers/api/src/objects/schedule.rs) keeps a hold:<ref> row in its timers table and arms its one alarm for the earliest. When it fires, the object decides Release with the reason hold_expired, and the aggregate (crates/domain/src/schedule.rs) frees the place only if the hold is still unpaid and its time has come. A duplicate or early alarm changes nothing, which matters because Cloudflare’s alarms, as read on 2026-11-06, run at least once and retry after a failure. A paid order’s timer is deleted in the same transaction as its confirmation.

Whose clock. A hold ends by business time, wall time plus the sandbox’s offset, so a visitor can advance the clock and watch every unpaid hold come due. A sandbox ends by wall time: next_wipe_at in crates/app/src/wipe.rs is the first 10:00 UTC after its birth, and the offset never moves it. Each end uses the clock of the thing it is about.

The wipe. The Sandbox has its own wipe timer, and the 10:00 UTC cron (Cloudflare’s cron triggers run on UTC, as read on 2026-11-06) asks Control to stamp every sandbox not yet wiped as due. Control wipes 50 a run from its own alarm and re-arms while rows remain, and the 15-minute cron stamps again any that did not finish. Each run ends in the wipe data point.

The backstop. Every object arms a backstop timer 36 hours after its first write (BACKSTOP_HOURS in wipe.rs; child.rs). When it fires the object logs backstop, deletes its alarm and its storage. This is a lease on the object itself. The nightly wipe depends on Control’s ledger and the Sandbox’s registry; the backstop depends only on the object’s own alarm, so a sandbox the wipe missed still ends, at most 36 hours after its objects were born.

A retry after the end

A lease can also end between a visitor’s request and its retry. If a placement is retried under the same key after its hold was released, by the timer or by the router giving the place back when the Order refused, the day no longer replays a stale “held”. The router first asks whether the Order exists, and if it does the Order replays its own answer. If not, the Schedule decides Retake (crates/domain/src/schedule.rs): the same place is held again with the retry’s own 15 minutes when there is room, and otherwise the answer is 409 hold-lost, naming the next free slot. Taking it back rather than refusing it is the choice docs/roadmap.md records for the hardening of 2026-11-06: the visitor asked once, and the place was free.

The payment that arrives after the end

The Order’s expiry and the Schedule’s hold timer are two timers on two objects at one instant, and either may fire first. Suppose the Schedule’s does, and the visitor’s payment is recorded while the Order is still placed. The Order accepts it and emits OrderPaid, and the Schedule’s Confirm finds the hold released.

The bakery takes the place back, past capacity if it must, and the event says revived: true. Money has moved, and the alternative is to refuse the place and refund. That is what the Order does once its own expiry has run: a payment after it emits OrderPaymentLate and a refund request. The line between the two is where the Order’s state says the payment landed. docs/roadmap.md (PR 6) lists the revival as a reading for Joseph to confirm, and it is the one way a count passes its capacity.

What it costs

Try it

Order from the menu and stop at the checkout. Sign in, open the console, advance the clock by 15 minutes, and read the capacity editor’s line for the slot: it says one fewer taken, and one more free. The console’s Deleted line counts down to the sandbox’s own end, and Wipe now ends it on the spot.

Sources

Back to the leases principle on How it’s built, to the decision that chose it, ADR 0005, or to the lab.