FinishLine is in active development. Features and availability may change before general release.

How clock sync works across a dozen devices

Every device continuously measures its offset from one shared time source, so times captured on separate phones are directly comparable.

Technical4 min readUpdated August 18, 2026

A meet timed on twelve phones is only meaningful if all twelve agree on what time it is. A finish captured on lane 4's device has to be comparable to a gun captured on a device 100 meters away, held by someone else, on a different carrier. This article explains how that agreement is established and maintained.

Two different accuracies

These get conflated constantly, so they are worth separating first.

Clock-domain accuracy is how closely the devices agree with each other, plus the latency between a physical event and the timestamp recorded for it. This is what the system controls, and the target is comfortably under 10 ms so it is never the limiting factor.

Physical-capture accuracy is how close a recorded time is to the instant the runner actually crossed. With a human pressing a button, this is bounded by reaction time, roughly 150 to 250 ms with about ±100 ms of spread, and no amount of clock quality improves it.

This is the same distinction track and field draws between hand timing and fully automatic timing. FinishLine's clock work makes the first number small; the acoustic start and photo finish are what attack the second.

One shared time source

The canonical time base is server time, defined by a small time service in the backend. Every device maintains its own running estimate of it.

This dissolves the obvious problem, which is that different phones use different time sources and drift apart. Devices stop caring about their own wall clocks entirely and reference one source.

The key property is transitivity: if the start device and a finish device are each within a few milliseconds of server time, they are within a few milliseconds of each other, with no device-to-device measurement at all. Nothing needs to be co-located, paired, or on the same network. Adding a thirteenth device costs nothing, because there is no mesh to expand.

Measuring the offset

Each device measures its offset from server time the way network time protocols do:

t0 = device time when the request was sent
t1 = server time when the request arrived
t2 = server time when the reply was sent
t3 = device time when the reply arrived

offset    = ((t1 - t0) + (t2 - t3)) / 2
roundTrip = (t3 - t0) - (t2 - t1)

The measurement runs as a burst of several exchanges, and the device keeps the sample with the smallest round-trip time. That sample spent the least time queued in the network, so its offset estimate is the cleanest. This matters over cellular, where round-trip times are both higher and far more variable than on a wired link.

On good signal this yields offsets accurate to single-digit or low double-digit milliseconds.

Why the monotonic clock, not the wall clock

The offset is built on each device's monotonic clock, the one that counts steadily since boot, never the wall clock.

A wall clock can jump sideways. The OS receives a network time correction, decides the device is 1.4 seconds off, and moves it. If a heat was in progress, every measurement spanning that jump is silently corrupted, and nothing about the resulting time looks wrong.

The monotonic clock never jumps. Its only quirk is an arbitrary zero point per device, which is exactly what the offset measurement resolves:

serverTime ≈ monotonicClock + offset

Drift, and why one sync is not enough

Device crystals differ by roughly 20 to 50 parts per million. At 30 ppm that is about 1.8 ms per minute, 100 ms per hour, and 400 ms over a four-hour meet. Far outside budget.

So each device maintains a running model rather than a one-time reading. It re-runs the offset measurement as a lightweight background poll every 10 to 30 seconds, and from those samples it estimates both its current offset and its own drift rate. Between polls, the drift estimate carries it; each poll corrects whatever accumulated.

This is also why brief connectivity loss is not a timing problem. The device knows how fast its own clock runs, so it keeps producing good timestamps across a gap in polling.

Uncertainty, and never lying about a time

Each device carries a sync-uncertainty value derived from recent round-trip quality and how long it has been since a good poll. When uncertainty crosses a threshold, the device polls harder and surfaces the condition.

If a device genuinely cannot keep itself synchronized, the response is to flag or hold its captures, marking their timing uncertainty as high, rather than recording a number the system cannot vouch for. That principle is worth stating on its own:

The system never records a time it cannot stand behind. A flagged time is a recoverable problem. A confidently wrong time is not.

There is no fallback time source, and that is deliberate. Two things bound the risk: the drift model covers short gaps, and there is enormous practical margin, because the dominant error in a hand-tapped finish is the human tap at around 100 ms. Even 15 to 20 ms of sync error on mediocre signal is well below the limiting factor.

Computing the official time

With every device on the same clock, the arithmetic is trivial:

officialTime = finishTime - gunTime
uncertainty  = sqrt(gunUncertainty² + finishUncertainty²)

Both inputs are absolute server-time values, and uncertainties combine in the usual way rather than being ignored.

The subtraction happens on the finish device, at the moment the operator commits the result. The finish timer already has the gun time, delivered over the relay, and the finish time it captured itself, so it sends a complete, pre-computed result. Nothing downstream recomputes it.

The detail that makes it all work

Capture the event timestamp, not the time your code runs.

A tap is timestamped by the OS when the touch occurred, not when the handler executes. An audio buffer carries a hardware capture timestamp. A camera frame carries a presentation timestamp. In each case the correct instant is available from the platform, already on the monotonic clock domain, and using it makes app-level processing latency irrelevant.

Get this wrong anywhere in the pipeline and no amount of clock synchronization saves the result.

Did this answer your question? If something here is wrong, missing, or out of date, tell us at support@finishlinetimer.com and we’ll fix it.

Search the knowledge base