A live quiz or game show only feels fair if every contestant sees the same ticking number. In a studio that uses a single source of truth (the host's monitor), that promise is easy to keep. In a remote setup — where producers, judges, and contestants sit behind different browsers on different networks — the same promise breaks in small, ugly ways: one player sees 10 seconds, another sees 11, the chat yells "lag!", and the round gets voided. This article is the engineering checklist I wish I had before I shipped our last remote trivia night, covering how to choose a countdown model that is forgiving of operator latency, how to defend it against the failure modes I have actually debugged, and how to verify the system end-to-end before you go live.
Pick the Right Clock Model Before You Pick a Tool
There are three clocks families count: from the trigger time (start at T, subtract Date.now() - T), or from a fixed end timestamp (T_end - Date.now()). For a quiz, the third family is the right default: all clients compute the same remaining time because they all reference the same T_end. The reference clock on each client is Date.now(), which is documented in the ECMAScript specification and returns milliseconds since the Unix epoch (ECMAScript 2024, Date). Because every modern browser implements Date.now() from a monotonic kernel clock, the values stay close across devices as long as the wall-clock time is correct.
That last clause is where most operators get burned. If the producer's laptop is 47 seconds fast because they never opened the system clock settings, every contestant will receive a "correct" answer that the server says is 47 seconds too early. The simplest defense is to have each client compute the round-trip offset against the same server just once at join, then use server_now ≈ client_now + offset for the rest of the round. RFC 5905, the Network Time Protocol, is the canonical reference for why this offset-then-serve pattern works and why you should not try to be cleverer than it (RFC 5905).
Why Visual Sync Is Not the Same as Logical Sync
Even with a perfect shared end timestamp, players see different numbers because rendering is per-frame. Three knobs drive that divergence:
- Frame rate. A 60 Hz monitor redraws every ~16.67 ms, a 120 Hz monitor every ~8.33 ms. The number you "see" is whatever the browser last painted before your retina caught up. Wikipedia has a good summary of the underlying timing model for screen refresh (Refresh rate).
-
Throttling. When a tab loses focus, browsers drop
requestAnimationFramecallbacks to roughly 1 Hz. The number freezes while the underlying timestamp keeps moving. When the player tabs back in, the painted number catches up in one frame, which produces the classic "it jumped" complaint. - Display lag. Many consumer monitors apply picture processing that adds 30–80 ms of input-to-photon delay. Two contestants on different TVs can disagree by a tenth of a second and both be technically correct.
Practical mitigation: show tenths of a second only in the final five seconds, when the human ear cannot catch sub-100ms differences anyway. Before the final five, show integer seconds rounded down, which absorbs both frame-rate drift and tab-throttling jitter.
The Operator's Latency Is the Hardest Variable
The single biggest source of disagreement in a remote quiz is not the player's machine — it is the host's finger. Between the moment the round timer visually hits 0 and the moment the host clicks "Stop accepting answers" there is a window where some answers are valid and others are not. If you accept anything with client_time <= 0, you have accepted whatever the slowest player's clock says. If you accept anything with server_time <= 0, you have accepted whatever the host's clock says. Neither is fair; you need a third option: a server-side deadline that the host cannot influence.
Concretely: when the host presses "Start round", the server writes T_end = server_now + 30_000 and signs it. When the host presses "Stop", the server ignores the press for scoring purposes — the deadline is the deadline. Any answer with server_received_at <= T_end counts; any later one does not. This is the same pattern used in HTTP Date and Age header reconciliation, where a single authoritative timestamp governs cache freshness regardless of when individual proxies observed the response (RFC 9111, HTTP Caching).
A Five-Point Pre-Broadcast Checklist
Before the broadcast goes live, run this in order. Each item should take less than a minute; do not skip even when you are in a rush.
- Clock sanity check. Open three different devices (laptop, phone, tablet), navigate to a clock page, and confirm they all agree to within one second. If any device drifts more than two seconds, fix NTP or correct the system clock before continuing.
-
Network round-trip. From the operator's seat,
curl -o /dev/null -s -w "%{time_total}\n" https://example.comfive times. Median round-trip above 200 ms means you should pin the server to a region near the majority of contestants. -
Tab-throttle rehearsal. Start a round, switch away for 30 seconds, switch back, and confirm the painted value matches the server's recomputed value to within one frame. If it does not, your display loop is reading from a cached DOM property instead of recomputing from
Date.now()each frame. - Hot-key rehearsal. Confirm the "Start round" and "Stop round" keys are not bound to anything the host uses for their normal software (screen share toggle, mute, push-to-talk). A misbinding here is the most common cause of "the round started two seconds early".
-
Replay check. Pull yesterday's session log and replay the timestamps: did
T_endget written before any client received the "round starting" event? If not, add a server gate before broadcasting.
Where a Shared Source Becomes Useful
For most rounds, you can build the deadline-and-render loop in roughly 60 lines of JavaScript using Date.now() plus a setInterval or requestAnimationFrame loop. The point at which a shared online countdown starts to pay for itself is when you need to display the same number to multiple people who cannot share a screen — a host monitor, a producer tally, a stream overlay, and a public lobby widget. Keeping four manual countdowns synchronized across four machines is exactly the kind of job that drifts by the second after ten minutes.
If that matches your setup, the step-by-step OBS countdown walkthrough on Lizely covers the OBS-specific layer (browser source, CSS, scene binding) on top of the server-deadline pattern above. Read it as the second half of this checklist rather than a replacement for it.
Debrief After Each Round
The most underused artifact in a remote quiz is the post-round log. For every round, store four numbers: T_start_server, T_end_server, the host's first press timestamp, and the latest client submission timestamp. After three rounds, you will see patterns:
- If
T_end_server - latest_submissionclusters near zero, your deadline is too tight and contestants are losing answers they thought were in. - If the host's press timestamp consistently trails
T_end_serverby more than two seconds, your host is reacting visually rather than trusting the audio cue. - If any client's submission timestamp is more than 500 ms after
T_end_server, that client is on a path with latency you should measure before the next event.
Treat these logs like a test suite. Re-run the same broadcast with the same numbers next week and confirm the deltas do not regress. The whole point of using a server-authoritative deadline is that it makes this kind of post-hoc analysis possible; if you cannot produce the four numbers from your last event, the deadline is not as authoritative as you think.
A Note on Accessibility and Color
A countdown that ticks down to zero is, by definition, a time pressure display. WCAG 2.2 SC 1.4.1 explicitly notes that color is not the only means of conveying information, so do not rely on "the number turns red" as your only signal. Pair color changes with a stroke, weight change, or position shift, and ensure the contrast ratio against the background clears 3:1 for non-text UI components (WCAG 2.2 Understanding SC 1.4.11). For the final five seconds, an audio tick at 1 kHz that drops to a lower pitch on the last beat gives contestants a second channel to react on, which matters more than color for players with reduced contrast sensitivity.
Frequently asked questions
How much clock drift between devices is acceptable?
For a 30-second round, anything over 500 ms of disagreement will produce visible disputes. Aim for less than 200 ms, which is what most consumer NTP clients achieve against a nearby pool. If you cannot measure it, assume it is worse than you think.
Should I use setInterval or requestAnimationFrame for the display loop?
Use requestAnimationFrame for the painted value, because it aligns with the screen refresh and pauses when the tab is hidden, which is the correct behavior for a quiz. Use setInterval only if you also need to fire audio cues at sub-second intervals, and even then, gate it behind a user gesture so the browser does not throttle it.
What is the minimum log I should keep per round?
Store T_start_server, T_end_server, host_press_at, latest_valid_submission_at, and the count of submissions rejected for being late. Five fields, no more. Anything richer belongs in a separate debug log you only turn on when investigating a complaint.
Can I just trust the player's Date.now() instead of running a server deadline?
You can, if and only if you have measured that every player's clock is within 200 ms of your server's. The cheapest way to maintain that guarantee is to compute and cache the offset on join, then use it for the rest of the session, exactly as NTP does.
This article was drafted with AI assistance and reviewed for technical accuracy before publishing.
Top comments (0)