prometheus/node_exporter · error
couldn't get SNTP reply
Error message
couldn't get SNTP reply: %w
What it means
The ntp collector's Update performs an SNTP query against the configured local NTP server (with the configured protocol version, IP TTL, and 1-second timeout). Any failure — network unreachable, TTL exceeded, timeout, or malformed reply — is wrapped in this message. Because Update errors surface as scrape failures, an unreachable server makes every scrape of this collector fail.
Solutions
- Verify a local NTP server is listening: 'chronyc statistics' / 'ss -ulpn | grep 123' and 'ntpdate -q 127.0.0.1'.
- Ensure the configured server is directly reachable at IP TTL=1 (same subnet/loopback); otherwise fix topology or (if you own the server) reconsider placement.
- Check firewall rules allow outbound UDP 123 and inbound replies.
- Point --collector.ntp.server at the correct local server IP.
- If NTP checks are not needed, disable with --collector.ntp or --disable-collector.ntp to stop scrape errors.
Example fix
// before: no local NTP server on 127.0.0.1 // --collector.ntp.server=127.0.0.1 -> scrape error every interval // after: run chrony/ntpd locally or disable the collector // chronyd (allow 127.0.0.1) // or // --disable-collector.ntp
Defensive patterns
Strategy: retry
Validate before calling
// shell: verify the local NTP server answers before enabling the collector chronyc -a 'tracker' >/dev/null 2>&1 || ntpq -c rv 127.0.0.1 >/dev/null 2>&1 || echo "no local NTP server on $NTP_SERVER"
Try / catch
// handle transient query failures without failing every scrape
if err := query(&resp); err != nil {
if isTimeout(err) || isRefused(err) {
c.logger.Warn("ntp query failed, will retry next scrape", "err", err)
return ErrNoData // degrade gracefully instead of erroring
}
return fmt.Errorf("couldn't get SNTP reply: %w", err)
} Prevention
- Run a local ntpd/chronyd before enabling --collector.ntp.
- Ensure the server is within one IP hop (TTL=1 probe) of the exporter.
- Allow UDP 123 both directions in host/firewall rules.
- Accept that the 1s timeout makes transient loss visible; alert on sustained failures only.
- Disable the collector on hosts where NTP checks are out of scope.
When it happens
Trigger: cgetQuery/ntp query returns an error during Update: the configured loopback/local NTP server is not running (UDP port closed), a firewall drops the reply, IP_TTL=1 is too low for a non-directly-connected server, or the 1-second timeout expires.
Common situations: Enabling --collector.ntp on hosts without a local ntpd/chronyd listening; pointing at a loopback IP where nothing listens on port 123 (connection refused/ICMP port unreachable); placing the exporter more than one hop from the server so TTL=1 probes never arrive; transient network loss during a scrape.
Related errors
- couldn't get udp queued bytes
- getifaddrs() failed
- getifaddrs() failed
- failed to initialize ethtool library
- could not get link modes
AI-assisted analysis of prometheus/node_exporter@17ddd77c59 (2026-09-07).
Data as JSON: /api/errors/acf082604adc725f.
Report an issue: GitHub.
Appendix: source
Thrown at collector/ntp.go:135
), prometheus.GaugeValue},
sanity: typedDesc{prometheus.NewDesc(
prometheus.BuildFQName(namespace, ntpSubsystem, "sanity"),
"NTPD sanity according to RFC5905 heuristics and configured limits.",
nil, nil,
), prometheus.GaugeValue},
logger: logger,
}, nil
}
func (c *ntpCollector) Update(ch chan<- prometheus.Metric) error {
resp, err := ntp.QueryWithOptions(*ntpServer, ntp.QueryOptions{
Version: *ntpProtocolVersion,
TTL: *ntpIPTTL,
Timeout: time.Second, // default `ntpdate` timeout
Port: *ntpServerPort,
})
if err != nil {
return fmt.Errorf("couldn't get SNTP reply: %w", err)
}
ch <- c.stratum.mustNewConstMetric(float64(resp.Stratum))
ch <- c.leap.mustNewConstMetric(float64(resp.Leap))
ch <- c.rtt.mustNewConstMetric(resp.RTT.Seconds())
ch <- c.offset.mustNewConstMetric(resp.ClockOffset.Seconds())
if resp.ReferenceTime.Unix() > 0 {
// Go Zero is 0001-01-01 00:00:00 UTC
// NTP Zero is 1900-01-01 00:00:00 UTC
// UNIX Zero is 1970-01-01 00:00:00 UTC
// so let's keep ALL ancient `reftime` values as zero
ch <- c.reftime.mustNewConstMetric(float64(resp.ReferenceTime.UnixNano()) / 1e9)
} else {
ch <- c.reftime.mustNewConstMetric(0)
}
ch <- c.rootDelay.mustNewConstMetric(resp.RootDelay.Seconds())
ch <- c.rootDispersion.mustNewConstMetric(resp.RootDispersion.Seconds())
View on GitHub (pinned to 17ddd77c59)