Most Linux servers run ntpd out of the box, drift around by a few milliseconds, and nobody complains — until they have to. Then you’re debugging Kerberos authentication failures, watching distributed traces fall apart because timestamps are off, or chasing a race condition that only reproduces on one node where the clock runs 8ms ahead.
Stock ntpd has been around since 1985. That’s not a dig at longevity — it’s a hint about the assumptions baked in: always-on hardware, clean networks, and a patience for convergence measured in hours. Modern workloads don’t have that patience. Chrony was written specifically to handle intermittent connectivity, VM clock skew, and fast convergence without sacrificing accuracy. On a half-decent LAN with the right upstream sources, getting stable sub-millisecond offset is genuinely achievable. On hardware with a PPS signal, you can hit microseconds.
This guide goes from zero to a properly tuned Chrony setup. Not the five-minute version — the one that holds up under load, survives VM migrations, and gives you enough monitoring visibility to trust your clocks.
Why Chrony Wins
Before touching config files, it’s worth understanding the actual architectural difference, because it changes how you configure things.
ntpd uses a fixed-step clock discipline algorithm. It adjusts the system clock in discrete jumps or via frequency steering at a relatively slow pace. It also maintains a rigid polling interval hierarchy and doesn’t adapt well when the network path to your time source gets noisy. Under typical conditions, expect 1–10ms offset. Under load or on a busy VM host, easily worse.
Chrony uses a regression-based algorithm (it models the clock’s drift as a linear function and corrects ahead of time rather than reactively). It adapts its polling interval based on measured stability — polling more aggressively when the clock is drifting and backing off when things are stable. It also handles the makestep case cleanly: on first start, it will step the clock immediately rather than slewing slowly toward the correct time over hours. That matters a lot in containers and VMs that boot with a stale hardware clock.
The practical outcome: Chrony converges in minutes rather than hours, and its steady-state accuracy on a good network is typically 10–100x better than ntpd.
Installation
On Debian/Ubuntu:
sudo apt update && sudo apt install -y chrony
On RHEL/Rocky/AlmaLinux 9:
sudo dnf install -y chrony
sudo systemctl enable --now chronyd
First thing: if ntpd or systemd-timesyncd are running, kill them before starting Chrony. Running two time daemons is a guaranteed way to have neither work correctly.
sudo systemctl disable --now ntpd ntp systemd-timesyncd 2>/dev/null || true
sudo timedatectl set-ntp false # disables systemd-timesyncd specifically
The Baseline Config: Better Than ntpd Out of the Box
The default config that ships with most distros is fine for a desktop. For a server you care about, swap it for this:
/etc/chrony/chrony.conf (Debian/Ubuntu path; on RHEL it’s /etc/chrony.conf):
# --- Time Sources ---
# Use pool.ntp.org with iburst for fast initial sync.
# Four servers gives Chrony enough to detect falsetickers.
pool 2.pool.ntp.org iburst minpoll 4 maxpoll 6
pool 3.pool.ntp.org iburst minpoll 4 maxpoll 6
# Alternatively, use your country-specific pools for lower latency:
# pool 0.ru.pool.ntp.org iburst minpoll 4 maxpoll 6
# --- Initial Convergence ---
# Allow a one-time step if offset > 1s at startup,
# otherwise slew (to avoid disrupting running apps).
makestep 1.0 3
# --- Drift File ---
# Stores measured clock drift between restarts — critical for fast re-sync.
driftfile /var/lib/chrony/drift
# --- RTC Sync ---
# Keep the hardware clock in sync; saves you on reboot.
rtcsync
# --- Log Configuration ---
logdir /var/log/chrony
log measurements statistics tracking
# --- Access Control ---
# Only allow localhost to query this instance (not acting as a server).
allow 127.0.0.1
allow ::1
# --- Leap Second Handling ---
# Use the kernel's leap smearing instead of a hard step.
leapsectz right/UTC
# --- Minimum Sources ---
# Require at least 2 sources to agree before trusting the result.
minsources 2
# --- Max Distance ---
# Reject sources with root distance > 1s (loose, tighten to 0.1 for stricter setups)
maxdistance 1.0
Reload and check:
sudo systemctl restart chronyd
chronyc tracking
You’ll see output like:
Reference ID : D4C0F0A1 (time.cloudflare.com)
Stratum : 3
Ref time (UTC) : Thu May 22 10:14:33 2026
System time : 0.000042341 seconds fast of NTP time
Last offset : +0.000038291 seconds
RMS offset : 0.000051224 seconds
Frequency : 12.841 ppm fast
Residual freq : -0.002 ppm
Skew : 0.018 ppm
Root delay : 0.021847423 seconds
Root dispersion : 0.000285631 seconds
Update interval : 64.2 seconds
Leap status : Normal
That System time line — 42 microseconds fast — is what you’re after. On a decent internet connection this is already achievable within a few minutes of starting Chrony. ntpd would take an hour to get there and still wouldn’t hold it as tightly.
Going Deeper: LAN NTP Server + Hardware Timestamping
If you’re running a cluster and need consistent sub-millisecond sync across all nodes, don’t send all of them to pool.ntp.org. The internet path has variable latency — 20ms one request, 80ms the next. You’ll never get tight sync from that.
The correct architecture: one or two dedicated NTP servers in your datacenter synchronized against good upstream sources (or a GPS/PPS reference), and then all your worker nodes talk to those. This is stratum 2 locally, but the delay is now 0.1ms across a switch rather than 40ms across the internet.
On the dedicated NTP server, add this to the config:
# Allow your internal network to use this as a time server
allow 10.0.0.0/8
allow 192.168.0.0/16
# If connectivity drops, continue serving time from local clock
# (at degraded accuracy rather than returning errors)
local stratum 10
# Hardware timestamping on your primary NIC
# Replace eth0 with your actual interface
hwtimestamp eth0
On client nodes that talk to your local server:
# Point directly at your NTP servers — not pools
server 10.0.1.10 iburst prefer minpoll 3 maxpoll 5
server 10.0.1.11 iburst minpoll 3 maxpoll 5
# Tighten polling for LAN (2^3=8s min, 2^5=32s max)
# This is aggressive — appropriate for a low-latency switch fabric
makestep 0.1 3
minsources 1
Hardware timestamping (hwtimestamp) is the big one. Software timestamping captures the send/receive time in the kernel’s network stack — after the packet has already bounced around interrupt handlers and scheduling queues. Hardware timestamping captures it at the NIC itself, before any of that latency. On a supported NIC, this alone can improve your offset stability by 5–10x.
Check if your NIC supports it:
ethtool -T eth0 | grep -E 'hardware|software'
You want to see hardware-transmit and hardware-receive in the capabilities. Most modern Intel and Mellanox NICs support this. Realtek doesn’t. Cheap VM virtio interfaces usually don’t either.
Stratum 1 with GPS + PPS
If you genuinely need microsecond-level accuracy — financial systems, telecoms, HPC — you want a GPS receiver with a Pulse Per Second (PPS) output feeding directly into your NTP server. The PPS signal is hardware-accurate to tens of nanoseconds. Combined with the GPS NMEA data for the coarse time-of-day, you get a true stratum 1 source with no internet dependency.
A cheap u-blox GPS module (NEO-6M, around €5) connected via USB gives you NMEA data. A GPIO pin or the DCD line of a serial port receives the PPS. Most of these modules output PPS on a dedicated pin.
Kernel support for PPS is built in as of Linux 3.0. Load the module for your connection method:
# For serial (most common with cheap USB adapters exposing a UART)
sudo modprobe pps_ldisc
# Verify the PPS device appeared
ls /dev/pps*
Then in Chrony config on the GPS server:
# NMEA source from GPS (coarse time, used to pick the right second)
refclock NMEA /dev/ttyUSB0 refid GPS precision 1e-1 offset 0.0 delay 0.2
# PPS source (the actual sub-second accuracy)
# Combined with SHM or directly via the kernel PPS device
refclock PPS /dev/pps0 lock GPS refid PPS precision 1e-7
# Don't pollute the pool — you're authoritative now
makestep 0.1 3
allow 0.0.0.0/0 # or your specific subnet
The lock GPS on the PPS line is important — it tells Chrony to only use PPS samples that are within a reasonable distance of what the NMEA source says. Without this, Chrony might pick up a PPS tick that’s off by an entire second and be confidently wrong by exactly 1s.
Monitoring: Trust But Verify
Running Chrony and assuming it works is how you end up with a Kerberos outage at 3am. Here’s the monitoring surface you should actually use.
Live tracking:
chronyc tracking
Key fields to watch: System time (current offset), RMS offset (stability over time), Skew (frequency uncertainty in ppm). If RMS offset is climbing, something is wrong — either a bad source, heavy CPU load, or a hypervisor stealing cycles.
Source health:
chronyc sources -v
The * prefix marks the currently selected source. + means usable backup, - means rejected (falseticker), ? means unreachable. If all you see are ?, your firewall is blocking UDP/123.
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^* time.cloudflare.com 3 6 377 27 +12us[ +15us] +/- 11ms
^+ time.google.com 1 7 377 53 -34us[ -31us] +/- 14ms
^+ ntp1.hetzner.de 2 6 377 91 +41us[ +44us] +/- 9ms
Long-term drift stats:
chronyc sourcestats -v
The Std Dev column tells you the standard deviation of offset for each source. A well-behaved internet server should show under 1ms. Your local GPS reference should be under 10 microseconds.
For automated monitoring, add this to your Prometheus/Nagios/whatever check:
# Returns offset in seconds, exit 1 if > 10ms off
python3 -c "
import subprocess, sys
out = subprocess.check_output(['chronyc', 'tracking']).decode()
for line in out.splitlines():
if 'System time' in line:
parts = line.split()
offset = float(parts[3])
direction = parts[4]
if direction == 'slow':
offset = -offset
print(f'offset={offset:.9f}')
sys.exit(0 if abs(offset) < 0.010 else 1)
"
Gotchas
VM clock instability. If your server is a VM, the hypervisor periodically pauses and resumes it. From Chrony’s perspective, time just jumped forward. The makestep directive handles large jumps at startup, but mid-run jumps confuse the frequency estimator. Add this to your VM configs:
# Allow stepping any time, not just at startup (risky on production DBs — know the tradeoff)
makestep 0.01 -1
The -1 means "always allow stepping, not just the first N adjustments." This is aggressive and can confuse applications that depend on monotonic time, but it’s better than a clock that’s 500ms off because the hypervisor stole CPU for two seconds.
Firewall blocking NTP. UDP port 123, outbound and inbound. Chrony queries look like regular NTP traffic, but some corporate firewalls redirect all UDP/123 to their own NTP servers transparently. If your offsets are suspiciously consistent but wrong, this might be why. Test with ntpdate -q <ip> to a known server and compare.
systemd-timesyncd resurfaces. Some distro updates re-enable systemd-timesyncd. It’s a minimal NTP client, not a full implementation, and it conflicts with Chrony. Add this to a systemd drop-in or your Ansible playbook:
sudo systemctl mask systemd-timesyncd
mask is stronger than disable — it prevents it from being started by any dependency chain, not just on boot.
The driftfile is sacred. Don’t delete /var/lib/chrony/drift in a "clean up the system" script. That file stores the measured clock frequency correction. Without it, Chrony starts cold every reboot and takes longer to converge. It’s a tiny file — leave it alone.
Wrong timezone ≠ wrong time. Chrony operates in UTC. timedatectl set-timezone adjusts display only, not what Chrony synchronizes. If your logs show timestamps that are one hour off, it’s a timezone issue in your application or shell, not in Chrony.
NTP reflection attacks. If you open your Chrony instance to the internet (e.g., allow 0.0.0.0/0), you’re a potential amplification target. NTP monlist responses used to be the classic DDoS vector. Chrony doesn’t support monlist by default, but still: don’t expose NTP to the internet unless you’re running a public NTP service on purpose. Use firewall rules to limit to your own subnets.
Production Checklist
Before calling this done, run through this mentally:
-
systemd-timesyncdandntpdare both masked, not just stopped -
chronyc trackingshows offset under 1ms, ideally under 0.1ms -
chronyc sourcesshows at least two sources with*or+status -
driftfileexists and is writable by the chrony user - Firewall allows UDP/123 outbound (and inbound if acting as server)
- Log rotation is configured for
/var/log/chrony - If on a VM:
makestepis tuned for your hypervisor’s pause behavior - If acting as a server:
allowis scoped to your actual subnet, not0.0.0.0/0 - Monitoring checks offset and alerts if it drifts past your acceptable threshold
Final Config Reference
Here’s the production-ready config in one place for a client node on a LAN with local NTP servers:
# /etc/chrony/chrony.conf — LAN client, tuned for sub-millisecond accuracy
server 10.0.1.10 iburst prefer minpoll 3 maxpoll 5
server 10.0.1.11 iburst minpoll 3 maxpoll 5
# Fallback if local servers die
pool pool.ntp.org iburst minpoll 6 maxpoll 8
makestep 0.1 3
driftfile /var/lib/chrony/drift
rtcsync
leapsectz right/UTC
minsources 2
maxdistance 0.3
logdir /var/log/chrony
log measurements statistics tracking
allow 127.0.0.1
allow ::1
And for the LAN NTP server with hardware timestamping:
# /etc/chrony/chrony.conf — Stratum 2 LAN server
pool 0.pool.ntp.org iburst minpoll 4 maxpoll 6
pool 1.pool.ntp.org iburst minpoll 4 maxpoll 6
pool 2.pool.ntp.org iburst minpoll 4 maxpoll 6
pool 3.pool.ntp.org iburst minpoll 4 maxpoll 6
makestep 1.0 3
driftfile /var/lib/chrony/drift
rtcsync
leapsectz right/UTC
# Hardware timestamping — replace with your interface name
hwtimestamp eth0
minsources 3
local stratum 10
# Serve your internal network
allow 10.0.0.0/8
allow 192.168.0.0/16
logdir /var/log/chrony
log measurements statistics tracking
Time synchronization is one of those infrastructure concerns that’s invisible until it isn’t. Chrony is better than ntpd by every metric that matters — convergence speed, steady-state accuracy, adaptability to real-world networks. There’s genuinely no reason to run ntpd on a fresh server in 2026. The config above will get you sub-millisecond accuracy on a decent LAN without any exotic hardware, and the GPS/PPS path is there when you need to go further.