louislam/uptime-kuma · warning · Error

Time offset ms exceeds threshold ms

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.

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.

Appendix: 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)