louislam/uptime-kuma · error · Error
Invalid NTP response: expected 48+ bytes, got
Error message
Invalid NTP response: expected 48+ bytes, got ${msg.length} What it means
Thrown by NTPMonitorType.parseNTPResponse when the UDP datagram is shorter than 48 bytes. A conformant NTPv3/v4 packet is exactly 48 bytes (plus optional extensions); anything shorter cannot contain the required headers, so the response is treated as malformed. This is raised inside the 'message' handler and surfaces via queryNTP's reject path.
Solutions
- Confirm the target truly runs NTP on UDP 123 (e.g. ntpdate -q <host> or ntpdig from another machine).
- Check firewalls/NAT between the monitor and target for packet rewriting/truncation.
- Try an IP address directly to rule out DNS pointing at the wrong host.
- Use a well-known public source (pool.ntp.org) to validate the monitor path, then reintroduce the target.
Example fix
# before: monitor.hostname = 'ntp.internal' (resolves to a non-NTP load balancer) # isolate: ntpdate -q ntp.internal # observe reply size / stratum # after: monitor.hostname = '10.0.0.53' (the real NTP appliance)
Defensive patterns
Strategy: try-catch
Validate before calling
// Sanity-check the target before relying on the monitor: // run: ntpdate -q <host> (a healthy server replies with a 48-byte packet and a real stratum) // Also confirm DNS resolves to a real NTP server, not a load-balanced non-NTP service.
Type guard
function isPlausibleNtpPacket(buf) { return Buffer.isBuffer(buf) && buf.length >= 48; } Try / catch
try { await ntpMonitor.check(monitor, heartbeat, server); }
catch (e) { if (/expected 48\+ bytes/.test(e.message)) { heartbeat.status = DOWN; heartbeat.msg = 'Malformed NTP response (target may not be an NTP server)'; } else throw e; } Prevention
- Confirm the target actually serves NTP on UDP 123 (use ntpdate/chronyc from another host).
- Rule out NAT/firewalls that rewrite or truncate UDP; try the IP directly to bypass DNS.
- Validate against pool.ntp.org first to confirm the monitor path works.
When it happens
Trigger: An ICMP port-unreachable or NAT/firewall rewrite returns a short UDP payload that is delivered to the socket, or a non-NTP service responds on UDP 123 with a truncated packet, or a spoofed/buggy device emits a partial NTP reply. msg.length < 48 then trips the guard.
Common situations: Firewall/NAT translating ICMP unreachable into a short datagram; querying a host that runs a different UDP service on 123; a middlebox truncating packets; a misbehaving embedded device; DNS resolved to an IP that is not actually an NTP server.
Related errors
- Hostname is required
- NTP server is unsynchronized (stratum 16)
- Root dispersion ms exceeds threshold ms
- Stratum meets or exceeds threshold
- Time offset ms exceeds threshold ms
AI-assisted analysis of louislam/uptime-kuma@6b5ea01557 (2026-08-12).
Data as JSON: /api/errors/deba34776e5707b6.
Report an issue: GitHub.
Appendix: source
Thrown at server/monitor-types/ntp.js:147
* @returns {Buffer} NTP request packet
*/
createNTPPacket() {
const packet = Buffer.alloc(48);
packet[0] = 0x1b;
return packet;
}
/**
* Parse an NTP response packet and calculate offset/delay
* @param {Buffer} msg NTP response packet (48+ bytes)
* @param {number} t1 Client originate timestamp in ms since NTP epoch (1900)
* @param {number} t4 Client receive timestamp in ms since NTP epoch (1900)
* @returns {object} Parsed NTP data including stratum, offset, refid, rootDispersion, roundTripDelay
* @throws {Error} If the packet is shorter than 48 bytes
*/
parseNTPResponse(msg, t1, t4) {
if (msg.length < 48) {
throw new Error(`Invalid NTP response: expected 48+ bytes, got ${msg.length}`);
}
const leapIndicator = (msg[0] >> 6) & 0x03;
const stratum = msg[1];
// Root dispersion: 32-bit unsigned fixed-point at offset 8, unit = seconds
const rootDispersionRaw = msg.readUInt32BE(8);
const rootDispersion = (rootDispersionRaw / 65536) * 1000;
// Reference ID: ASCII for stratum 0-1, IPv4 address for stratum 2+
let refid;
if (stratum <= 1) {
refid = msg.toString("ascii", 12, 16).replace(/\0/g, "").trim();
} else {
refid = `${msg[12]}.${msg[13]}.${msg[14]}.${msg[15]}`;
}
// Server receive timestamp (T2) at offset 32View on GitHub (pinned to 6b5ea01557)