Clock Drift on Game Servers: Why Tick Timers Skew and How to Measure Offset Against an NTP Pool

A game server keeps three different clocks and quietly assumes they agree. The monotonic clock drives the tick loop, the wall clock stamps logs and bans, and the NTP-disciplined system clock is what every external system believes. When they drift apart, the symptoms look unrelated: cooldowns that expire early, ban timestamps in the future, scheduled events that fire twice, and anti-cheat replays that fail to correlate against server logs.

Drift is not a rare fault. A default Linux install with no time daemon can wander hundreds of milliseconds a day on a busy VM, and a hypervisor that pauses a guest for live migration can jump the clock forward by seconds at a time. This article explains where the skew comes from, how to measure offset properly, and how to choose a tolerance per game genre.

Why game servers care about offset at all

Most gameplay timers are counters, not timestamps, so a 100-millisecond offset is invisible. Problems start when timestamps cross a system boundary: a matchmaking service checks a cooldown by comparing a stored epoch to its own clock, an admin tool auto-unbans at a specific time, a replay parser aligns client events against server events. Each of those is a comparison between two machines.

The second class of problem is correlation. When a player appeals a ban, the only evidence is a log line with a timestamp. If the game server is 40 seconds ahead of the auth service, the timeline reconstructs incorrectly and the appeal becomes an argument rather than a check.

Sources of drift on a game node

The oscillator is the first source: a typical commodity crystal is accurate to about 50 parts per million, which is roughly 4.3 seconds per day before any correction. Datacenter temperature changes make that worse, and a rack that runs hot in the afternoon will drift differently than one that does not.

Virtualisation adds two more. Clock interrupts are tickless and may be coalesced, so a guest’s clock can lag under heavy steal time; and live migration, snapshots, and host suspend can step the clock discontinuously. A bare-metal node avoids most of this — but only if its own time daemon is actually running and reachable.

Measuring offset with chronyc and ntpq

If the node runs chrony, chronyc tracking gives the current offset against the selected source, plus the estimated frequency error and the root delay of the path to the reference. A healthy node sits under one millisecond on a wired datacenter path; anything persistently above ten milliseconds deserves investigation.

On systems still running classic ntpd, ntpq -p lists peers with an offset column in milliseconds. Check the reach register: a peer showing 377 is fully reachable, while anything less means recent packets were lost and the offset figure is stale rather than measured.

# chrony: current offset and frequency error
$ chronyc tracking
Reference ID    : 8A3C1B22 (ntp1.example.net)
System time     : 0.000412 seconds fast of NTP time
Last offset     : +0.000388 seconds
RMS offset      : 0.000621 seconds
Frequency       : 12.431 ppm slow
Root delay      : 0.002145 seconds

# classic ntpd peers; reach 377 = fully reachable
$ ntpq -p
     remote           refid      st t when poll reach   delay   offset  jitter
*ntp1.example.net  .GPS.        1 u   41   64  377    0.412   +0.388   0.221
+ntp2.example.net  .PPS.        1 u   33   64  377    0.531   -0.204   0.310

# independent drift measurement over 600 s (ppm = delta_ms / 600000 * 1000)
$ o0=$(ntpdate -q pool.ntp.org | tail -1 | awk '{print $9}'); t0=$(date +%s)
$ sleep 600
$ o1=$(ntpdate -q pool.ntp.org | tail -1 | awk '{print $9}')
$ awk -v a=$o0 -v b=$o1 -v dt=$(($(date +%s)-t0)) 'BEGIN{printf "drift %.1f ppm\n",(b-a)/dt*1e6}'

# per-source frequency and skew
$ chronyc sourcestats -v | head -12

Trust the frequency estimate more than any single offset sample. A node showing 12 ppm slow will accumulate about one second of error per day if correction stops, which tells you how quickly it will matter if the daemon dies.

Pick an NTP pool topology that does not fight you

Point the node at four sources from two different operators, or at your provider’s own time service plus two public sources. A single-source configuration cannot detect a falseticker, and a provider time service adds a very short network path, which lowers the noise floor that limits achievable accuracy.

Prefer authenticated sources where available. NTP without authentication is spoofable, and on a game node a forged time packet can move ban-expiry logic. If you cannot authenticate, restrict the daemon to specific source addresses and alert on changes to the selected reference ID.

Tick timers that should not depend on wall clock

Anything measured in elapsed time — cooldowns, spawn intervals, anti-cheat rate windows — should use the monotonic clock, which never steps backward. Engines that use a wall-clock delta for the tick loop will produce a burst of catch-up ticks the moment NTP corrects a large offset, which players experience as a one-second teleport.

Use CLOCK_MONOTONIC or the language equivalent for every interval and reserve wall time for display, logging, and persistence. Where a persisted timestamp must be compared later, store the wall time plus the measured offset at write time so the comparison can be corrected retroactively.

Acceptable offset by genre and tick rate

Tolerance is set by how much timestamp precision affects cross-system decisions, not by tick rate alone. A 128-tick shooter has very precise in-engine timing but almost no cross-system dependency on wall clock, while a management game with an auction house can tolerate a slow tick but not sloppy absolute time.

Set alert thresholds three to five times above the tolerance so one noisy sample does not page anyone. Page on sustained offset over several minutes, or on a frequency error that implies fast drift, rather than on an instantaneous reading.

Genre / use case Tick rate Acceptable offset Alert above
Competitive FPS 64–128 Hz < 5 ms 20 ms
Battle royale / matchmaking 20–30 Hz < 10 ms 30 ms
Survival sandbox 20 Hz < 50 ms 150 ms
MMO with auction house 10–20 Hz < 20 ms 60 ms
Bans, appeals, audit logs n/a < 20 ms 50 ms
Async / management sim 1–5 Hz < 250 ms 1 s

Monitoring drift as a metric, not a one-off check

Export offset, frequency error, and source count from every node into your metrics pipeline and graph them next to tick time. Drift correlates with hypervisor pressure: a node whose clock suddenly starts wandering is often a node whose host is oversubscribed or being migrated, which will affect tick stability in the same window.

Keep a graph of offset-minus-offset-one-hour-ago; a step change of more than a few milliseconds identifies clock steps the daemon smoothed over and that other systems will see as a discontinuity. Review it after every host maintenance window, since that is when steps occur. For bare-metal hardware where clock discipline and tick stability both depend on uncontended cores, see the game-ready configurations with measured clock and storage details and the broader hardware notes for sustained game workloads.

Leave a Reply