Files
2026-10-11 12:01:16 -07:00

3.4 KiB
Raw Permalink Blame History

Windows clock sync (w32time) for latency-gated work

Reusable failure mode. Diagnosed 2026-10-11 on Tony's desktop (physical ASUS box) while building a latency-gated paper harness whose spec voids any candidate whose NTP skew exceeds 250 ms.

Symptom

Anything that gates on the local clock fails: an NTP gate rejects every candidate, timestamps look wrong, and the clock drifts instead of holding. On this box the offset went from ~−1.8 s, to ~+10 ms right after a sync, then slid to −284 ms and kept going (~4–5 ms/min).

Diagnose

sc.exe query w32time            # STATE: 1 STOPPED == no time sync at all
w32tm /query /status            # Leap Indicator, Stratum, Last Successful Sync, Poll Interval

Then measure the offset independently of w32time (a stdlib NTP query against pool.ntp.org, time.google.com, time.cloudflare.com). Multiple servers agreeing confirms it is a real local-clock error, not one bad source. HTTP Date headers are useless for sub-second work — they are whole-second granularity.

Root causes seen (in order encountered)

  1. The service was stopped. w32time not running ⇒ no sync at all.
  2. Registry writes went to the wrong key path. MinPollInterval, MaxPollInterval, MaxAllowedPhaseOffset, LocalClockDispersion live under HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config — not the service root (…\Services\W32Time). reg add reported success but nothing applied. SpecialPollInterval/NtpServer live under …\W32Time\TimeProviders\NtpClient.
  3. Default poll is 1024 s, so a bad RTC drifts far between syncs.
  4. MaxAllowedPhaseOffset = 1 (s) makes w32time slew, not step — it never nulls a sub-second residual. The clock sat stable but stuck at ~−180 ms.

Fix (all elevated)

set CFG=HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config
set NTP=HKLM\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient
sc config w32time start= auto
w32tm /config /manualpeerlist:"time.google.com,0x1 time.cloudflare.com,0x1 pool.ntp.org,0x1" /syncfromflags:manual /reliable:no /update
reg add "%CFG%" /v MinPollInterval /t REG_DWORD /d 6 /f      REM 64 s
reg add "%CFG%" /v MaxPollInterval /t REG_DWORD /d 10 /f
reg add "%CFG%" /v LocalClockDispersion /t REG_DWORD /d 3 /f
reg add "%CFG%" /v MaxAllowedPhaseOffset /t REG_DWORD /d 0 /f REM force a STEP to null the residual
reg add "%NTP%" /v SpecialPollInterval /t REG_DWORD /d 64 /f  REM honoured only with the 0x1 flag
net stop w32time & net start w32time
w32tm /resync /force

Result: −284 ms drifting → ±2 ms stable (held over 150 s across four NTP servers).

Lessons

  • A reg add that "completed successfully" can be a no-op if the key path is wrong. Verify with w32tm /query /configuration, never with the command's exit code.
  • MaxAllowedPhaseOffset: 0 forces a step on every sync — right for nulling a residual; if constant stepping is unwanted, raise it to a small tolerance afterwards.
  • Post-step, w32tm /query /status can read Leap Indicator: 3 (not synchronized) / Stratum 0 while the time is actually correct — trust an independent NTP measurement, not the status flag.
  • Date headers (1 s granularity) can never satisfy a sub-second freshness/skew gate; measure freshness from local timestamps instead.
  • Requires elevation (UAC). A physical box with a bad RTC has no host time-integration to fall back on, unlike a VM.