Clock skew across distributed systems is a silent operational hazard. When local server clocks drift out of synchronization, microservices fail to correlat
Clock skew across distributed systems is a silent operational hazard. When local server clocks drift out of synchronization, microservices fail to correlate logs, distributed databases suffer split-brain scenarios or data corruption during timestamps-based conflicts, and security protocols like OAuth 2.0 or Kerberos reject authentication tokens due to time-window mismatches.
While background daemons like chronyd or systemd-timesyncd maintain local time, they can silently freeze, get blocked by firewall rules, or suffer from misconfiguration without raising immediate system alerts. This NTP Time Sync Monitor independently measures your system clock drift against target NTP servers, reports latency metrics, and exits with non-zero status codes when drift exceeds tolerable thresholds—making it ideal for cron jobs, monitoring agents, or container readiness probes.
The script queries one or more NTP (Network Time Protocol) servers using the Network Time Protocol library (ntplib). For each target server, it sends standard SNTP UDP packets (port 123) and calculates:
The script evaluates the calculated offsets against a user-configured threshold (--alert-offset-ms). Results can be emitted in human-readable tabular text or structured JSON for log parsing pipelines. If any queried NTP server reports a time drift exceeding the threshold, or if all requested NTP servers fail to respond, the script exits with status code 1, signalling an alert condition to upstream orchestration tools.
#!/usr/bin/env python3
"""
NTP Time Sync Monitor
Queries upstream NTP servers, checks local system clock offset,
and alerts if time drift exceeds specified thresholds.
"""
import argparse
from dataclasses import asdict, dataclass
import json
import os
import sys
from typing import List, Optional
try:
import ntplib
except ImportError:
sys.exit(
"Error: 'ntplib' dependency missing. Install it using 'pip install ntplib'."
)
@dataclass
class NTPCheckResult:
server: str
stratum: int
offset_ms: float
delay_ms: float
status: str # "OK", "OFFSET_EXCEEDED", "UNREACHABLE"
error: Optional[str] = None
def query_ntp_server(
server: str, timeout: float, alert_offset_ms: float
) -> NTPCheckResult:
"""Queries a single NTP server and calculates time offset and delay."""
client = ntplib.NTPClient()
try:
# Request NTP packet (version 3 is widely supported)
response = client.request(server, version=3, timeout=timeout)
# Convert offset and delay from seconds to milliseconds
offset_ms = response.offset * 1000.0
delay_ms = response.delay * 1000.0
stratum = response.stratum
# Determine status based on drift threshold
status = "OFFSET_EXCEEDED" if abs(offset_ms) > alert_offset_ms else "OK"
return NTPCheckResult(
server=server,
stratum=stratum,
offset_ms=round(offset_ms, 3),
delay_ms=round(delay_ms, 3),
status=status,
)
except (ntplib.NTPException, Exception) as err:
return NTPCheckResult(
server=server,
stratum=0,
offset_ms=0.0,
delay_ms=0.0,
status="UNREACHABLE",
error=str(err),
)
def format_text_output(results: List[NTPCheckResult], alert_offset_ms: float) -> str:
"""Formats NTP check results into a human-readable table."""
lines = [
f"NTP Time Sync Status (Threshold: ±{alert_offset_ms} ms)",
"=" * 70,
f"{'SERVER':<25} {'STATUS':<17} {'OFFSET (ms)':<12} {'DELAY (ms)':<10} {'STRATUM'}",
"-" * 70,
]
for r in results:
if r.status == "UNREACHABLE":
lines.append(
f"{r.server:<25} {r.status:<17} {'N/A':<12} {'N/A':<10} N/A ({r.error})"
)
else:
lines.append(
f"{r.server:<25} {r.status:<17} {r.offset_ms:<12.3f} {r.delay_ms:<10.3f} {r.stratum}"
)
lines.append("=" * 70)
return "\n".join(lines)
def parse_args() -> argparse.Namespace:
"""Parses command-line arguments and merges defaults from environment variables."""
default_servers = os.environ.get(
"NTP_SERVERS", "pool.ntp.org,time.google.com,time.cloudflare.com"
)
default_threshold = float(os.environ.get("NTP_ALERT_OFFSET_MS", "100.0"))
default_timeout = float(os.environ.get("NTP_TIMEOUT", "5.0"))
parser = argparse.ArgumentParser(
description="Monitor system clock drift against upstream NTP servers."
)
parser.add_argument(
"--servers",
type=str,
default=default_servers,
help=f"Comma-separated list of NTP servers (default: {default_servers})",
)
parser.add_argument(
"--alert-offset-ms",
type=float,
default=default_threshold,
help=f"Max acceptable offset in milliseconds before alerting (default: {default_threshold})",
)
parser.add_argument(
"--timeout",
type=float,
default=default_timeout,
help=f"Socket timeout in seconds per server query (default: {default_timeout})",
)
parser.add_argument(
"--json",
action="store_true",
help="Output check results in JSON format",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
# Split server comma-separated list into cleaned string list
server_list = [s.strip() for s in args.servers.split(",") if s.strip()]
if not server_list:
print("Error: No NTP servers specified.", file=sys.stderr)
sys.exit(2)
results: List[NTPCheckResult] = []
has_alert = False
reachable_count = 0
for server in server_list:
res = query_ntp_server(server, args.timeout, args.alert_offset_ms)
results.append(res)
if res.status != "UNREACHABLE":
reachable_count += 1
if res.status == "OFFSET_EXCEEDED":
has_alert = True
# Alert condition if all servers failed or if threshold exceeded
all_failed = reachable_count == 0
if all_failed:
has_alert = True
# Output generation
if args.json:
output_data = {
"summary": {
"total_queried": len(server_list),
"reachable": reachable_count,
"threshold_ms": args.alert_offset_ms,
"alert": has_alert,
},
"results": [asdict(r) for r in results],
}
print(json.dumps(output_data, indent=2))
else:
print(format_text_output(results, args.alert_offset_ms))
if all_failed:
print("ALERT: All NTP servers were unreachable!")
elif has_alert:
print("ALERT: Local clock drift exceeds threshold on one or more servers!")
# Non-zero exit code indicates unhealthy sync status or network failure
if has_alert:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
The script supports configuration via command-line arguments or environment variables. Command-line flags take precedence over environment variables.
| Parameter | Environment Variable | Default Value | Description |
|---|---|---|---|
--servers |
NTP_SERVERS |
pool.ntp.org,time.google.com,time.cloudflare.com |
Comma-separated list of hostname/IP target NTP servers. |
--alert-offset-ms |
NTP_ALERT_OFFSET_MS |
100.0 |
Maximum absolute clock offset allowed (ms) before triggering an alert exit. |
--timeout |
NTP_TIMEOUT |
5.0 |
Network socket timeout (seconds) per NTP query. |
--json |
N/A | False |
When present, formats the output as a valid JSON object. |
Run the script using target servers and standard threshold defaults:
python3 ntp_monitor.py
Output:
NTP Time Sync Status (Threshold: ±100.0 ms)
======================================================================
SERVER STATUS OFFSET (ms) DELAY (ms) STRATUM
----------------------------------------------------------------------
pool.ntp.org OK 1.234 24.102 2
time.google.com OK 0.811 18.450 1
time.cloudflare.com OK -0.412 12.301 1
======================================================================
Target specific upstream servers with a strict threshold of 15 milliseconds:
python3 ntp_monitor.py --servers pool.ntp.org,time.apple.com --alert-offset-ms 15
Output:
NTP Time Sync Status (Threshold: ±15.0 ms)
======================================================================
SERVER STATUS OFFSET (ms) DELAY (ms) STRATUM
----------------------------------------------------------------------
pool.ntp.org OK 2.110 22.501 2
time.apple.com OFFSET_EXCEEDED 18.452 45.120 1
======================================================================
ALERT: Local clock drift exceeds threshold on one or more servers!
(Exit code: 1)
Use the --json flag to emit machine-readable metrics for vector agents, Datadog, or custom alerting scripts:
python3 ntp_monitor.py --servers time.google.com --json
Output:
{
"summary": {
"total_queried": 1,
"reachable": 1,
"threshold_ms": 100.0,
"alert": false
},
"results": [
{
"server": "time.google.com",
"stratum": 1,
"offset_ms": 0.412,
"delay_ms": 15.201,
"status": "OK",
"error": null
}
]
}
ntplib upon launch and exits with actionable installation steps if absent.ntplib.NTPException is caught gracefully per host, marking that server as UNREACHABLE without crashing execution.has_alert = True and exits with code 1 to avoid false-positive success reporting.http.server module to expose standard /metrics endpoints for Prometheus scraping (e.g., node_ntp_offset_milliseconds).--auto-fix flag that executes /usr/sbin/chronyc step or systemctl restart systemd-timesyncd when severe drift (> 1000ms) is observed.