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

  1. On the target, verify the daemon: 'chronyc sources' / 'ntpq -p' and confirm a reachable, selected peer.
  2. Allow outbound NTP (UDP 123) from the target to its upstreams, or point it at reachable internal stratum-1/2 servers.
  3. Wait for the daemon to select a source and re-stabilise (can take minutes).
  4. 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

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


AI-assisted analysis of louislam/uptime-kuma@6b5ea01557 (2026-08-12). Data as JSON: /api/errors/d4e66c2c08d4c83f. Report an issue: GitHub.