Shared clock
Some state needs no syncing at all once everyone agrees on the time. An enemy walking a fixed route at a fixed speed is in the same place on every screen if every client knows when it set off; a countdown ends together if every client knows when it ends. Sending the position ten times a second over presence works, but it is bandwidth spent on a fact each client could compute. What those clients lack is a clock they agree on.
The connection clock is that clock. It is off until the app asks for it:
client.clock.start();
// Later, on any client of the same server:await client.clock.ready(); // synced: now() reads server timeconst waveStartsAt = client.clock.now() + 3_000; // write this into a document// …and each frame, on every client:const elapsed = client.clock.now() - game.waveStartsAt;client.clock.now() is server time, in milliseconds since the Unix epoch. Write
it into documents, compare it across clients, compute from it on your own frames.
Until the first answer it is the local clock, so wait on client.clock.ready()
before writing a time other clients will compute from; it resolves once the
clock has synced, at once if it already has.
How it estimates
Section titled “How it estimates”A started clock sends clock:ping over the connection and the server answers
clock:pong with its own time
(wire protocol). From one round trip the client
knows the server read its clock somewhere between sending and receiving, and
takes the midpoint, so a single sample is off by at most half its round trip.
It takes five samples back to back on every connect, then one a minute, and
keeps the offset from the fastest round trip among its recent samples (NTP’s
minimum-delay filter): the shortest trip leaves the least room for the two legs
to differ. client.clock.status reports synced, the offsetMs and the
rttMs of the sample it came from; client.clock.subscribe hears every change.
now() is the local monotonic clock plus that offset, and a better sample does
not make it jump: it slews to the new offset, running up to 10% fast or slow
until it has caught up, the way NTP and game clocks do. So now() never runs
backwards, and a 20 ms correction is spread over 200 ms. Only the first sync,
and a correction over 250 ms, step at once; status.offsetMs is always the
estimate itself, which now() may still be catching up with.
Offline now() keeps counting on the last offset. A reconnect samples afresh,
since a new connection may take a different path, but the old estimate stays
in the running. Each sample is off by at most half its round trip, so a slow
first answer whose range overlaps the old estimate’s cannot replace it. One
whose range does not proves the old estimate wrong (the local clock paused
while the laptop slept, say), and wins at once.
Opt-in, per connection
Section titled “Opt-in, per connection”A client that never calls start() sends no clock traffic, which matters on a
Durable Object: the heartbeat is
answered at the edge without waking the object, but a clock ping is a real
message the object handles. start({ burstSamples, resyncIntervalMs, sampleTimeoutMs }) tunes the cadence; resyncIntervalMs: null samples on
connect only.
The clock belongs to the connection, not to presence or to a document: one clock serves every channel and every document the client touches.
In one page
Section titled “In one page”In the in-memory demos every client shares
the page’s clock, so the offset is near zero by construction; a LatencyLink
delays both directions alike, which the midpoint cancels. To see the clock
correct something, give a client a skewed local clock:
connectInMemoryClient(connection, schemas, { localClock: () => performance.timeOrigin + performance.now() + 2_000,});Gaps & open questions
Section titled “Gaps & open questions”- Agreeing on time is not agreeing on outcomes. A computed enemy still dies
where the authority says it does, and that news arrives one-way latency late.
Draw slightly in the past, as games do: Contingency draws half the clock’s
rttMsplus a tick behindnow(), so a kill has arrived before the enemy walks past it. - Precision is the network’s. The estimate is as good as the most symmetric recent round trip; a path whose legs are consistently lopsided biases it by half the difference, and nothing on the client can see that.