louislam/uptime-kuma · error · Error
NTP server is unsynchronized (stratum 16)
Error message
NTP server is unsynchronized (stratum 16)
What it means
Thrown when the NTP response's stratum byte equals 16. Per RFC 5905, stratum 16 is reserved for 'unsynchronized' — the server's clock is not currently disciplined by a reference source, so its time cannot be trusted. This is checked before the configurable stratum threshold because stratum 16 is a hard failure regardless of policy.
Source
Thrown at server/monitor-types/ntp.js:36
const startTime = dayjs().valueOf();
if (!monitor.hostname) {
throw new Error("Hostname is required");
}
const port = monitor.port || 123;
const timeout = (monitor.timeout || 10) * 1000;
const ntpResult = await this.queryNTP(monitor.hostname, port, timeout);
heartbeat.ping = dayjs().valueOf() - startTime;
const { stratum, offset, rootDispersion, refid, roundTripDelay } = ntpResult;
heartbeat.msg = `Stratum: ${stratum}, RefID: ${refid}, Offset: ${offset.toFixed(3)}ms, Delay: ${roundTripDelay.toFixed(3)}ms, Dispersion: ${rootDispersion.toFixed(3)}ms`;
if (stratum === 16) {
throw new Error("NTP server is unsynchronized (stratum 16)");
}
const stratumThreshold = monitor.ntp_stratum_threshold || 5;
if (stratum >= stratumThreshold) {
throw new Error(`Stratum ${stratum} meets or exceeds threshold ${stratumThreshold}`);
}
const offsetThreshold = monitor.ntp_time_offset_threshold || 1000;
if (Math.abs(offset) >= offsetThreshold) {
throw new Error(`Time offset ${offset.toFixed(3)}ms exceeds threshold ${offsetThreshold}ms`);
}
const dispersionThreshold = monitor.ntp_root_dispersion_threshold || 500;
if (rootDispersion >= dispersionThreshold) {
throw new Error(
`Root dispersion ${rootDispersion.toFixed(3)}ms exceeds threshold ${dispersionThreshold}ms`
);
}View on GitHub (pinned to 6b5ea01557)
Solutions
- On the target, verify the daemon: 'chronyc sources' / 'ntpq -p' and confirm a reachable, selected peer.
- Allow outbound NTP (UDP 123) from the target to its upstreams, or point it at reachable internal stratum-1/2 servers.
- Wait for the daemon to select a source and re-stabilise (can take minutes).
- Point the monitor at a different, known-good NTP source.
Example fix
# on target server, before: # chronyc sources -> 0 sources (^?) # fix upstream reachability, then: sudo chronyc makestep # verify: chronyc tracking -> 'Stratum' becomes 2/3
Defensive patterns
Strategy: try-catch
Validate before calling
// Probe the target before relying on the monitor: // run: ntpdate -q <host> or chronyc -h <host> sources // and reject targets reporting stratum 16.
Try / catch
try { await ntpMonitor.check(monitor, heartbeat, server); }
catch (e) { if (/stratum 16/.test(e.message)) { heartbeat.status = DOWN; heartbeat.msg = e.message; } else throw e; } Prevention
- Confirm the target daemon has selected a healthy peer (chronyc sources / ntpq -p).
- Don't monitor a freshly-restarted NTP server until it converges.
- Prefer known-good public or internal stratum-1/2 servers.
When it happens
Trigger: queryNTP resolves, the server replies with a valid >=48-byte packet, parseNTPResponse returns stratum===16. Happens when ntpd/chronyd on the target lost all sources, just started, has no peers, or its system peer was marked unreachable.
Common situations: Target server's own NTP daemon is broken (no upstream, firewall blocking outbound 123); freshly rebooted server before sync; GPS/appliance NTP source disconnected; querying a host that is not really an NTP server but happens to answer on UDP 123.
Related errors
- Hostname is required
- Stratum ${stratum} meets or exceeds threshold ${stratumThres
- Time offset ${offset.toFixed(3)}ms exceeds threshold ${offse
- Root dispersion ${rootDispersion.toFixed(3)}ms exceeds thres
- Invalid NTP response: expected 48+ bytes, got ${msg.length}
AI-assisted analysis of louislam/uptime-kuma@6b5ea01557 (2026-08-12).
Data as JSON: /api/errors/d4e66c2c08d4c83f.
Report an issue: GitHub.