louislam/uptime-kuma · error · Error
Invalid NTP response: expected 48+ bytes, got ${msg.length}
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.
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)
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)
- Stratum ${stratum} meets or exceeds threshold ${stratumThres
- Time offset ${offset.toFixed(3)}ms exceeds threshold ${offse
- Root dispersion ${rootDispersion.toFixed(3)}ms exceeds thres
AI-assisted analysis of louislam/uptime-kuma@6b5ea01557 (2026-08-12).
Data as JSON: /api/errors/deba34776e5707b6.
Report an issue: GitHub.