Why not systemd-timesyncd
systemd-timesyncd is an SNTP client — it polls one server, sets the clock, sleeps. It does not discipline the local oscillator continuously, doesn't track drift, can't serve time to other hosts, and stops working cleanly if its single upstream becomes unreachable. Fine for a laptop that's online most of the time. Inadequate for a server.
Install
# Debian / Ubuntu (uninstalls timesyncd in the process)
sudo apt install chrony
# RHEL / Fedora
sudo dnf install chrony
# Arch
sudo pacman -S chrony
# On Debian/Ubuntu, make sure timesyncd is disabled
sudo systemctl disable --now systemd-timesyncd 2>/dev/null || true
The default config
Distros ship a reasonable default in /etc/chrony/chrony.conf (Debian) or /etc/chrony.conf (RHEL). The minimum useful version:
# Use a public pool. The "iburst" flag speeds up the initial sync.
pool 2.pool.ntp.org iburst
# Or pick a specific source — e.g. Cloudflare's stratum-1 anycast
# server time.cloudflare.com iburst nts
# Adjust system clock immediately if off by >1 second within the first 3 updates
makestep 1.0 3
# Use kernel hardware-clock keeper if available (typical on real hardware)
rtcsync
# Drift file remembers the local oscillator's offset across reboots
driftfile /var/lib/chrony/chrony.drift
# Logging
logdir /var/log/chrony
Reload:
sudo systemctl restart chronyd
sudo systemctl enable chronyd
Verify it's working
chronyc sources -v
chronyc tracking
chronyc sourcestats
chronyc tracking output worth understanding:
Reference ID : A0B97F76 (160.183.127.118)
Stratum : 2
Ref time (UTC) : Sun Sep 5 12:34:56 2025
System time : 0.000012345 seconds slow of NTP time
Last offset : +0.000023456 seconds
RMS offset : 0.000034567 seconds
Frequency : 12.345 ppm slow
Residual freq : +0.001 ppm
Skew : 0.045 ppm
Root delay : 0.012345 seconds
Root dispersion : 0.001234 seconds
Update interval : 64.0 seconds
Leap status : Normal
- System time — current offset from NTP. Under 1 ms on a wired LAN; tens of ms on Wi-Fi.
- RMS offset — smoothed average offset; tracks how steady the sync is.
- Frequency — ppm correction chrony is applying to the local oscillator. The drift file remembers this so the next boot starts already-corrected.
- Root delay / Root dispersion — latency to the ultimate time source and an estimate of accumulated uncertainty.
Pick better sources
The pool.ntp.org defaults are fine for most purposes. For latency-sensitive setups, two upgrades:
- Use a stratum-1 anycast source. Cloudflare's
time.cloudflare.comsupports NTS (Network Time Security, RFC 8915) which authenticates and encrypts time-sync exchanges — relevant when an attacker on the path could otherwise feed you bogus time:
server time.cloudflare.com iburst nts
- Add a local time server on the LAN. If you have OPNsense (see that tutorial) or any always-on host, run chrony in server mode there and point internal hosts at it — sub-millisecond accuracy on the LAN, no public-internet exposure for every server's clock.
Run chrony as a private LAN time server
On the box that should serve time to the LAN:
# Add to chrony.conf, then restart
allow 192.168.0.0/16
allow 10.0.0.0/8
# Optionally serve to clients if the LAN goes offline by trusting the local clock
local stratum 10
On every other host:
# Replace the public pool with the LAN server
server time.lab.example.com iburst
Now LAN hosts get tighter synchronization (one local hop instead of multi-stratum public NTP) and the gateway is the only box reaching out to the public time pool.
Virtual machines
VMs have a notoriously messy time story — the host's clock interrupts get delivered late, paused, or coalesced, so the guest's monotonic counters drift independently of wall time. Two rules:
- Run chronyd in every VM, pointed at a stable source (the host, the LAN, or NTP). systemd-timesyncd cannot handle the resulting larger offsets cleanly.
- Tell the hypervisor the guest is doing its own time discipline. On KVM/QEMU, add a paravirtual clock (
kvm-clockis default) and disable the host's clock-injection; on VMware, disable "VMware Tools time synchronization".
For Proxmox VMs, the default config does both correctly.
Laptops that suspend
The drift file is the critical piece: it captures the local crystal's known frequency offset, so when the system wakes from suspend, chrony re-applies the known drift correction in the first few seconds rather than waiting for several poll cycles to re-converge. On modern Debian / Ubuntu, this just works; verify driftfile /var/lib/chrony/chrony.drift is set and that the file is being updated.
Monitor drift via Prometheus
node_exporter (see Prometheus tutorial) exposes time-sync metrics out of the box:
# PromQL: max offset across the fleet
max by (instance) (abs(node_timex_offset_seconds))
# Alert if any host is >100ms off
- alert: HostClockSkew
expr: abs(node_timex_offset_seconds) > 0.1
for: 5m
A clock-skew alert catches "this VM has been quietly drifting for a week" before it becomes a TLS-cert-expired-tomorrow problem at 4am.
What chrony is not
- Not PTP (Precision Time Protocol). For sub-microsecond synchronization on a LAN (trading, scientific instruments, broadcast video), use ptp4l from linuxptp instead. chrony is the right answer for "millisecond-class wall-clock accuracy with internet upstreams."
- Not a time-zone fixer. Wall-clock display is set by
timedatectl set-timezoneindependently of chrony's work.