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 32

View on GitHub (pinned to 6b5ea01557)

Solutions

  1. Confirm the target truly runs NTP on UDP 123 (e.g. ntpdate -q <host> or ntpdig from another machine).
  2. Check firewalls/NAT between the monitor and target for packet rewriting/truncation.
  3. Try an IP address directly to rule out DNS pointing at the wrong host.
  4. 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

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


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