louislam/uptime-kuma · warning · Error

Time offset ${offset.toFixed(3)}ms exceeds threshold ${offse

Error message

Time offset ${offset.toFixed(3)}ms exceeds threshold ${offsetThreshold}ms

What it means

Thrown when abs(offset) >= monitor.ntp_time_offset_threshold (default 1000 ms). 'offset' is the client-server clock offset computed per RFC 5905: ((T2-T1)+(T3-T4))/2. A large absolute offset means the Uptime Kuma host's clock and the target NTP server disagree beyond the allowed tolerance.

Source

Thrown at server/monitor-types/ntp.js:46

        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`
            );
        }

        heartbeat.status = UP;
    }

    /**
     * Query an NTP server via UDP
     * @param {string} hostname NTP server hostname or IP
     * @param {number} port NTP server port (usually 123)
     * @param {number} timeout Timeout in milliseconds
     * @returns {Promise<object>} Parsed NTP response data

View on GitHub (pinned to 6b5ea01557)

Solutions

  1. Enable time sync on the Uptime Kuma host (install chrony/ntp, or enable the hypervisor's time integration).
  2. Point the monitor at a geographically closer NTP server to cut asymmetric delay.
  3. If the skew is expected/benign, raise monitor.ntp_time_offset_threshold.
  4. Run 'chronyc tracking' / 'ntpq -p' on the monitoring host to confirm its own offset is small.

Example fix

// before: monitor.ntp_time_offset_threshold = 1000  (cross-region link)
// after:  monitor.ntp_time_offset_threshold = 5000
// and on the kuma host: sudo apt-get install -y chrony && sudo systemctl enable --now chrony
Defensive patterns

Strategy: validation

Validate before calling

function validateOffsetThreshold(monitor) {
  const t = Number(monitor.ntp_time_offset_threshold ?? 1000);
  if (!Number.isFinite(t) || t <= 0) throw new Error('ntp_time_offset_threshold must be > 0');
}

Prevention

When it happens

Trigger: queryNTP succeeds, |offset| >= offsetThreshold. Caused by the monitoring host's clock drifting (common on VMs/containers without working time sync), large asymmetric network delay, or the target itself serving skewed time.

Common situations: Container/VM host without ntpd/chronyd, so it drifts; high-latency or jittery WAN to the NTP server; monitoring a stratum-1 across an intercontinental link; host clock stepped by kvmclock/PTP incorrectly.

Related errors


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