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
- A cap of 14 days. The offset only grows, and never past 1,209,600 seconds (set as
MAX_OFFSET_Sincrates/domain/src/clock.rs). There is no way back until the wipe gives the visitor a new sandbox the next day. - A quote you cannot watch expire. A quote lasts 14 open days, 16 or more calendar days, so no advance within the cap expires one made since birth;
docs/roadmap.md(PR 7) leaves the choice open, a higher cap or a shorter validity. - A call to every child on every move. A seeded sandbox has 36 children (from
docs/cost.md, as built on 2026-11-06), so one click is at least 36 requests to other objects, plus a row written for each timer that moves. - Two clocks to keep apart. Every timer is written with the clock it belongs to, and a mistake is silent: a business deadline set on wall time never moves.
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
- Time, Clocks, and the Ordering of Events in a Distributed System, Leslie Lamport, Communications of the ACM 21(7), July 1978, the copy on the author’s site, read on 2026-11-06: the happened-before relation, and logical clocks as counters that need no relation to physical time.
- Durable Objects alarms, Cloudflare’s Durable Objects documentation, read on 2026-11-06: one alarm at a time per object, set in milliseconds since the Unix epoch, with a new setAlarm() replacing the old one; at-least-once execution, retried with exponential backoff from 2 seconds, up to 6 retries.
- Cron Triggers, Cloudflare’s Workers documentation, read on 2026-11-06: a cron trigger runs on UTC.
- Receive Stripe events in your webhook endpoint, Stripe’s documentation, read on 2026-11-06: a signed timestamp on every webhook, checked against a tolerance of five minutes by default in Stripe’s libraries.
- The lab, the lab index on ihaveasegway.com, read on 2026-11-06: five lab pages, the Route, Queue, Cache, Backfill and Raft Labs, and none on clocks or time travel.
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.