How it’s built

Why the bakery’s clock is yours to move

Most of what a bakery does is about later. This is how a visitor moves later closer: one offset the bakery owns, timers that know which clock they belong to, and the things that must never take the bakery’s time.

A week you cannot wait for

A visitor reaches the checkout and stops. In 15 minutes of bakery time the hold on their pickup place ends. A catering inquiry from last week is due a follow-up on the third open day after its quote. A campaign is scheduled for tomorrow morning. Each of these is real code with a timer, and none of it can be seen in a visit of ten minutes. The console therefore has a Clock with four buttons: 15 minutes, 1 hour, 1 day and Next opening, which goes to the next 7:00 on a day the bakery opens.

What simpler designs do, and where they break

One design fires the timers by hand: a debug button that runs “expire this order now”. It shows the result and skips the cause. The order’s events would say it expired at a time when its hold had not ended, and every kind of timer would need a button of its own. Another moves the whole Worker’s clock. That moves every sandbox at once, and it moves things that are not the bakery’s to move: a cookie’s expiry, Stripe’s signature check, the nightly wipe. 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 clocks.

Two clocks, and who owns each

The bakery keeps two clocks and never confuses them. Wall time is the platform’s. Business time is wall time plus the sandbox’s offset, whole seconds the Sandbox object keeps as clock_offset_s. One adapter reads the platform’s clock (inworkers/api/src/clock.rs), and its two functions are the whole conversion: business_at adds the offset to a wall time, and wall_at takes it away. Business time is shown at America/Denver’s offset for that instant, so a sandbox born in summer reads -06:00 and its seeded history agrees (docs/roadmap.md, PR 6).

The name is borrowed. Lamport’s logical clocks, in his 1978 paper, are counters with no tie to physical time: they exist to order events. The bakery’s business time is physical time shifted. The Lamport-like part is beside it: a counter, clock_v, that the Sandbox adds one to on every move, so that any object can tell a newer offset from an older one.

Later is the bakery’s question

The platform’s alarm knows only wall time: Cloudflare’s setAlarm(), as read on 2026-11-06, takes milliseconds since the Unix epoch, and an object has one alarm at a time. So every object keeps a timers table, and a timer that belongs to the business carries two times: due_business, when the bakery wants it, and due_wall, when the platform should wake the object to ask (inworkers/api/src/objects/stream.rs). The alarm is armed for the earliest due_wall. When it fires, the object reads business time and decides what is due. The alarm only wakes the object. Whether a hold has ended is decided by the aggregate, by business time.

A click on 1 day sends the Sandbox an AdvanceClock. In one transaction it grows the offset by 86,400 seconds, adds one to clock_v, appends ClockAdvanced and stores the answer under the request’s key. Then it calls /_clock on every registered child whose clock_v is behind (inworkers/api/src/objects/child.rs, ClockUpdate). Each child writes the new offset and recomputes due_wall from due_business for every timer that has one, then arms its alarm for the earliest. A day’s advance makes every unpaid hold’s wall time the past, so the alarm fires at once and the holds end in business-time order. A child that cannot be reached keeps its old clock_v in the Sandbox’s registry; the Sandbox logs clock_children_missed and its own clocktimer tries again 30 seconds later. An update no newer than the one a child holds changes nothing, so the retries are safe.

Three steps are lengths of business time. A day is 86,400 seconds, so across a change to or from daylight time the local hour shifts by one. Next opening is the exception, worked out by the calendar (crates/domain/src/clock.rs), so from a Sunday evening it lands on Tuesday at 7:00, Monday being closed.

What keeps the wall clock, and why

Anything that answers to someone outside the sandbox stays on wall time. The wipe at 10:00 UTC is a cron trigger, and cron triggers run on UTC (Cloudflare, read on 2026-11-06); it is a promise on the privacy page and cannot be a visitor’s to move. A cookie and a Turnstile token expire by the browser’s and Cloudflare’s clocks. Stripe stamps its webhooks with its own time, and the bakery checks the signature within 300 seconds of wall time (TOLERANCE_S in crates/app/src/stripe/webhook.rs); in business time, a webhook after any advance would look a day old and be refused. The Analytics Engine points count the real account’s use. The loop guard’s 100 re-arms an object a day, and Control’s 30 minutes of simulated traffic a day, count UTC days, so skipping ahead never buys a fresh budget. The 36-hour backstop and every event’s recorded_at are wall time as well.

A click that arrives twice

Each click is one intent with its own Idempotency-Key, and the buttons are disabled while one is in flight (apps/web/src/lib/clock.ts, ClockClicks). If the answer is lost, the next click on the same step sends the same key and the Sandbox replays the first answer, so the clock moves once. Two clicks that were both answered are two moves, which is what the visitor asked for. The key is for a request repeated by the network, not for a visitor who presses twice on purpose.

What it costs

Try it

Sign in, order from the menu and stop at the checkout. In the console, press 15 minutes under Clock and read the capacity editor’s line for that slot: one place fewer is taken. Press 1 day and read the activity feed for ClockAdvanced, then keep pressing until the clock refuses with its limit of 14 days.

Sources

Back to the time-travel principle on How it’s built, to the decisions that chose it, ADR 0004 and ADR 0005, or to the lab.