📑 Daftar Isi
- The Three Candidates: chrony vs ntpd vs systemd-timesyncd
- Step 1: Diagnose Your Time Sync Before Touching Anything
- Step 2: Clear Out the Competition
- Step 3: Install Chrony
- Step 4: Configure chrony.conf Like a Pro
- Step 5: Verify Sync — The Part Everybody Skips
- Step 6: Fix the Timezone and the Hardware Clock
- Step 7: Open Up the Firewall for NTP
- Step 8: The Troubleshooting Table You'll Actually Use
- Pro Tips & Warnings From the Trenches
Fixing Linux Time Sync with Chrony and NTP: A Complete Troubleshooting Guide
Okay, I have to tell you about this before I forget, because it’s one of those moments where everything just clicks. Yesterday a client’s staging server was throwing “certificate expired” errors on every single API call. We renewed the cert, reinstalled it, triple-checked the chain — still broken. And then the engineer helping me casually ran timedatectl, and I watched him go quiet for a second. The server clock was off by eleven minutes. ELEVEN MINUTES. That’s all it was. One forgotten time sync daemon, and a whole stack of services acting insane.
And here’s the crazy part — this isn’t rare at all. I run into it constantly, and every single time it feels like finding out the house alarm was tripped by a spider on the sensor. The fix is genuinely simple once you know what you’re looking at. So let me show you exactly how I fix Linux time sync issues using chrony and NTP on production servers, step by step, the way I wish someone had shown me years ago. Trust me, you’re gonna want to bookmark this one.
Alright, let’s back up and talk about why this matters, because I want you to feel the weight of it. Think of your server like a bakery. Every station has its own timer — the ovens have one, the proofing box has one, the guy prepping dough has a stopwatch. As long as all the timers roughly agree, the bakery runs fine. But the moment one oven runs three minutes slow, the batches that came out of it get logged at the wrong time, the delivery schedule falls apart, and the owner starts blaming the wrong recipe. That’s your server with a broken clock. Logs get wrong timestamps, cron fires at the wrong moment, TLS certificates look expired, and database replicas refuse to agree with each other. Time sync is the silent foundation underneath all of it.
Now, why does the clock drift at all? Your server has two clocks: a hardware clock (the RTC, powered by a battery on physical machines) and a system clock managed by the kernel. The hardware clock drifts because it’s just a crystal oscillator — cheap ones wander a few seconds per day. The system clock is derived from it, and on virtual machines it gets even worse because the hypervisor can pause, migrate, or inflate the virtual clock. Without a daemon to periodically reconcile against an authoritative time source, your clock just wanders off over days and weeks.
That’s what NTP is for — the Network Time Protocol. Your machine asks an authoritative time server (or a pool of them) “hey, what time is it?”, measures the network round-trip, and steers the local clock. There are three implementations you’ll meet in the wild: the classic ntpd, systemd-timesyncd, and chrony. Here’s the takeaway I want you to remember for this whole article: on a modern Linux box, especially a VM, chrony is almost always the right answer. It handles the weird stuff VMs do — suspended clocks, live migration, bursty latency — far more gracefully than the alternatives. This is the tool you want running in production. And if your box lives on a virtualized platform, you might also enjoy our VPS KVM migration guide once you’re done here.
The Three Candidates: chrony vs ntpd vs systemd-timesyncd
Let me lay out the differences quickly, because people get stuck here way more than they should.
| Feature | chrony | ntpd | systemd-timesyncd |
|---|---|---|---|
| Best for | VMs, containers, laptops, servers with flaky network | Physical enterprise servers, NTP broadcast networks | Desktops and workstations where good enough is fine |
| Fast initial sync | Yes (iburst makes it snappy) | Slow-ish, gradual | Yes, but coarse |
| Handles suspended or frozen VM clock | Excellent — detects and corrects | Poor | Poor |
| Can act as full NTP server | Yes | Yes | No |
| Configuration | Simple, one file | More complex | Nearly none |
| Default on | RHEL 8/9, Rocky/Alma 9, Ubuntu 22.04+, Fedora | Older RHEL/CentOS, older Ubuntu | Ubuntu fallback, most minimal installs |
Quick verdict: if your box is a VM (which most “servers” are these days), run chrony. If it’s a physical box with nothing special going on, chrony still works great. systemd-timesyncd is honestly fine for a laptop you use to browse the web. The moment it’s production, make it chrony. Done talking — let’s fix some clocks.
Step 1: Diagnose Your Time Sync Before Touching Anything
I can’t stress this enough: look before you leap. Half the broken time-sync situations I’ve debugged were caused by someone reconfiguring a daemon that wasn’t even the problem. First thing, SSH in and check the lay of the land:
date
timedatectl status
Read the output carefully. The two lines that matter are System clock synchronized and NTP service. Here’s what a healthy box looks like:
Local time: Mon 2026-08-03 09:15:22 WIB
Universal time: Mon 2026-08-03 02:15:22 UTC
RTC time: Mon 2026-08-03 02:15:22
Time zone: Asia/Jakarta (WIB, +0700)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
A few things to read off this output:
- System clock synchronized: yes — some daemon is steering the clock, and it’s succeeding.
- NTP service: active — systemd knows about the NTP configuration.
- RTC in local TZ: no — the hardware clock is kept in UTC. This is what you want. Never store local time in the RTC on Linux.
If yours says synchronized: no, or the NTP service is inactive, we’ve found our problem. Next, figure out which daemon (if any) is actually running:
systemctl list-units --type=service | grep -E "chronyd|ntpd|ntp|systemd-timesyncd"
Or check each one explicitly:
systemctl status chronyd
systemctl status systemd-timesyncd
systemctl status ntpd

You’ll land in one of three situations:
- chronyd is active — then the issue is probably in the configuration or the firewall. Jump to Steps 4 and 7.
- systemd-timesyncd is active — it works, but for production I’d still move to chrony. Proceed to Steps 2 and 3.
- Nothing is running — this is the most common one on freshly provisioned VPS boxes. Install chrony (Step 3) and we’re golden.
Every now and then you’ll find TWO daemons fighting each other — chronyd plus systemd-timesyncd both enabled. That’s a recipe for a jittery clock, because they’ll step on each other’s corrections. One time-sync daemon per box. That’s the rule.
Step 2: Clear Out the Competition
If systemd-timesyncd is running and you want chrony to take over, stop and disable it. But before you go killing services, let’s be adults about this — back things up and know exactly what you’re changing.
Security Warning: Back Up Before Proceeding
Before you stop any service or edit time-sync config on a production server, make sure you have:
- A backup of the current config:
cp /etc/chrony/chrony.conf /etc/chrony/chrony.conf.bak.$(date +%Y%m%d) - If you’re migrating from ntpd, back up
/etc/ntp.conftoo. - Know exactly which daemon is active right now (
systemctl list-units | grep ntp) so you can roll back if needed.
Stopping the wrong service without a plan can leave your box with no time sync at all — and during business hours that means wrong cron jobs, broken cert validation, and confusing logs for everyone.
Now, if systemd-timesyncd is in the way:
sudo systemctl stop systemd-timesyncd
sudo systemctl disable systemd-timesyncd
And if you’re on an older box running the classic ntpd:
sudo systemctl stop ntpd
sudo systemctl disable ntpd
Verify nothing else is holding the port: ss -uap | grep 123 should come back empty. If something’s still listening, hunt it down before moving on.
Step 3: Install Chrony
Choose your distro’s flavor:
Ubuntu / Debian:
sudo apt update
sudo apt install chrony -y
Rocky / AlmaLinux / RHEL 8+:
sudo dnf install chrony -y
CentOS 7 / RHEL 7:
sudo yum install chrony -y
Then enable it to start on boot and start it now:
sudo systemctl enable --now chronyd
sudo systemctl restart chronyd
Restarting right after a fresh install is low-risk since the config is still the package default. If you’ve already hand-edited the config, back it up first (see the warning box in Step 2) before you restart.
Step 4: Configure chrony.conf Like a Pro
The config lives at /etc/chrony/chrony.conf. Here’s the setup I use for production boxes, with the important directives called out:
# Time sources — pick a nearby pool, use iburst for fast initial sync
pool 0.id.pool.ntp.org iburst
pool 1.id.pool.ntp.org iburst
pool 2.id.pool.ntp.org iburst
# makestep is the hero of this whole file.
# makestep 1 3 means: if the offset is more than 1 second, step the clock
# instead of slewing, and only for the first 3 updates.
# This is what saves VMs whose clock is minutes off after a pause.
makestep 1 3
# Refuse sources that are too far out of range
maxdistance 16.0
# Optional: make this box a time source for your LAN
# allow 10.10.10.0/24
# local stratum 10
# Logging for audit purposes
logdir /var/log/chrony
log measurements statistics tracking
Let’s zoom in on the two directives that actually matter:
pool ... iburst— your time sources. I use the Indonesian pool here since the box sits in Jakarta; if your server is in Europe or the US, plainpool ntp.org iburstis fine and chrony sorts it out. Theiburstflag fires a burst of requests at startup so you sync in seconds instead of minutes.makestep 1 3— without this, chrony only slews the clock (nudges it gradually). If the offset is eleven minutes, like my story above, slewing would take forever. With makestep, if the offset exceeds one second within the first 3 updates, chrony jumps the clock straight to the right time. On a VM just resumed from a snapshot, this is the difference between a healed clock and a confusing one.
Save the file when you’re done, then restart and confirm it came up clean:
sudo systemctl restart chronyd
sudo systemctl status chronyd
You want to see active (running). If you get any Unknown directive warnings, there’s a typo in the config — fix it, restart again, verify again.
Step 5: Verify Sync — The Part Everybody Skips
Here’s the thing nobody tells you: installing chrony is step one, but verifying that it actually works is what separates the people who fix the problem from the people who think they fixed it. Two commands rule this domain: chronyc tracking and chronyc sources -v.
First, the tracking report:
chronyc tracking
Healthy output looks like this:
Reference ID : 203.0.113.10 (ntp1.id.pool.ntp.org)
Stratum : 2
Ref time (UTC) : Mon Aug 03 02:15:22 2026
System time : 0.000034 seconds slow of NTP time
Last offset : +0.000021 seconds
RMS offset : 0.000031 seconds
Frequency : -0.415 ppm slow
Residual freq : +0.001 ppm
Skew : 0.002 ppm
Root delay : 0.021012 seconds
Root dispersion : 0.002456 seconds
Update interval : 1024.3 seconds
Leap status : Normal
Let’s read it line by line, because this is where people get lost:
- Reference ID — the source chrony is currently locked onto. In my example the real pool IP is masked, but you should see a real source name here. If it shows 0.0.0.0 or nothing, no source is selected yet.
- Stratum: 2 — you’re two hops from an atomic clock. Healthy. Stratum above 5-6 usually means poor sources or a bad network path.
- System time: 0.000034 seconds slow — your clock is within 34 microseconds of the reference. Excellent.
- Leap status: Normal — no leap second pending. You’ll occasionally see Insert second around June or December when the real world adds leap seconds; don’t panic.
Now the source list, which is honestly the more useful command:
chronyc sources -v
.-- Source mode '^' = server, '=' = peer, '#' = local clock.
/ .- Source state '*' = current synced, '+' = combined , '-' = not combined,
|/ '?' = unreachable, 'x' = time may be in error, '~' = time too variable.
|| .-+-+-+-+-+-+-+-+-+-+-+-
|| MS Name/IP address Stratum Poll Reach LastRx Last sample
==============================================================================
^* ntp1.id.pool.ntp.org 2 6 377 35 +111us[ +164us] +/- 11ms
^+ ntp2.id.pool.ntp.org 2 6 377 35 +202us[ +254us] +/- 12ms
^+ ntp3.id.pool.ntp.org 2 6 377 35 -156us[ -150us] +/- 13ms
Decoding time:
- The
*in the state column marks the source currently being used, and the^in the mode column means it’s a server. If you see NO star anywhere, chrony hasn’t selected a source — keep digging. - Reach: 377 — that octal value is your packet recovery score. 377 means all 8 recent polls got replies. Dropping numbers like 177, 37, or 3 mean packets are being lost — almost always firewall or routing.
- Last sample — recent offset in microseconds. Values in the millisecond range are fine. Consistently half a second or more? Something’s off in the network path.
Want more detail? chronyc sourcestats gives you long-term statistics per source, and chronyc ntpdata shows raw NTP packet info if you’re feeling brave. For a final sanity check, run timedatectl status again and confirm System clock synchronized: yes.
That’s the core of it, right there. But we’re not done — a broken timezone or a locked-down firewall can undo all of this, so let’s handle those two landmines too.
Step 6: Fix the Timezone and the Hardware Clock
You can have a perfectly synced UTC clock and still see wrong time everywhere if the timezone is off. Set it properly:
sudo timedatectl set-timezone Asia/Jakarta
timedatectl status
Browse available zones with timedatectl list-timezones. While you’re here, make sure the RTC runs in UTC, never local time:
sudo timedatectl set-local-rtc 0
sudo hwclock --systohc
hwclock --systohc writes the current system time into the hardware clock. On physical boxes this keeps the battery-backed RTC sane; on VMs the RTC is virtual and mostly managed by the hypervisor, but running it once after a big correction doesn’t hurt.
Step 7: Open Up the Firewall for NTP
If chrony is installed, configured, restarted, and your chronyc sources still shows question marks (state ?), the firewall is almost certainly blocking UDP port 123. It happens on default-deny setups all the time.
UFW (Ubuntu/Debian):
sudo ufw allow out 123/udp comment 'ntp client'
sudo ufw allow 123/udp comment 'ntp server'
firewalld (Rocky/Alma/CentOS):
sudo firewall-cmd --add-service=ntp --permanent
sudo firewall-cmd --reload
CSF (common on cPanel/WHM boxes):
# Edit /etc/csf/csf.conf and add port 123 to UDP_IN and UDP_OUT,
# then apply the changes:
csf -r
One nuance: if your server is only a client (the common case), you only strictly need outbound UDP 123. Inbound only matters if you’re also serving time to other machines. I usually open both anyway — it makes the box ready to be a time source later without a second firewall change.
Step 8: The Troubleshooting Table You’ll Actually Use
This is the table I wish existed when I was a junior. Bookmark it — 80% of time-sync incidents are in here somewhere.
| Symptom | Likely cause | Fix |
|---|---|---|
All sources show state ? |
UDP 123 blocked, or two daemons fighting | Open UDP 123 (Step 7), stop the other daemon (Step 2) |
| NTP active but synchronized: no | chronyd can’t reach its pool, or bad config | Check chrony.conf has a valid pool ... iburst line |
| Clock off by minutes or hours | VM was suspended or migrated, or makestep missing | Add makestep 1 3, or force a step with chronyc makestep |
| Slow but steady drift (seconds per day) | Cheap hardware RTC, virtualization clock skew | Keep RTC in UTC, run hwclock --systohc, leave chronyd running |
| Reach count dropping (377 to 177 to 37) | Lost NTP packets, congestion, or dying source | Check ss -uap | grep 123, add more sources, swap pool |
| Fine after fix, wrong again after reboot | chronyd not enabled, or timesyncd came back | systemctl enable --now chronyd, keep timesyncd disabled |
| Clock fine, apps still complain about certs | Wrong timezone on the box | Set the right timezone (Step 6) |
If you’ve been through all the steps and something still feels weird, go read the chrony log at /var/log/chrony/chronyd.log. Here’s a healthy sequence and what each part tells you:
2026-08-03 02:14:01Z chronyd version 4.3 starting (+CMDMON +NTP +REFCLOCK +RTC +PRIVDROP +SCFILTER +SIGND +ASYNCDNS +NTS +LEAPSECMOM +SUNWARN)
2026-08-03 02:14:01Z chronyd using configuration from /etc/chrony/chrony.conf
2026-08-03 02:14:01Z Listening on UDP port 123
2026-08-03 02:14:01Z Initial frequency -415.300 ppm
2026-08-03 02:14:06Z System clock wrong by 42.3 seconds, adjustment started
2026-08-03 02:14:06Z System clock was stepped by 42.3 seconds
2026-08-03 02:14:36Z Selected source 203.0.113.10 (ntp1.id.pool.ntp.org)
2026-08-03 02:15:06Z Source 203.0.113.10 (ntp1.id.pool.ntp.org) is reachable
2026-08-03 02:15:07Z System clock wrong by 0.0003 seconds, adjustment started
2026-08-03 02:15:07Z System clock was slewed by 0.0003 seconds
2026-08-03 02:15:10Z Source 203.0.113.11 (ntp2.id.pool.ntp.org) is reachable
Walk through it top to bottom — this is the symptom-to-pattern-to-root-cause path:
- Lines 5-6: System clock wrong by 42.3 seconds followed by System clock was stepped — that’s makestep doing its job. The clock jumped straight to the right time. If you never see a stepped line, makestep isn’t triggering (or wasn’t set) and your clock is being nudged slowly instead.
- Line 7: Selected source — chrony picked its primary reference.
- Lines 8 and 10: is reachable — sources are answering. If you see unreachable here, it’s your firewall or network path, not chrony.
Pro Tips & Warnings From the Trenches
A few scars worth sharing:
Tip 1: Never store local time in the hardware clock. RTC in local TZ: no is the goal, always. If you set it to local, a daylight-saving change or a timezone edit turns into a confusing clock catastrophe, and dual-boot setups get even messier.
Tip 2: On VMs, don’t rely on systemd-timesyncd alone. It syncs once at boot and then just sort of vibes. Chrony is purpose-built for the weird clock behavior virtual machines exhibit — use it.
Warning: Migrating from ntpd to chrony mid-business-hours can cause the clock to step (jump) by the full offset. For time-sensitive applications — distributed locks, replay protection, any monotonic-time assumptions — that jump can matter. Do the swap during a maintenance window, not at 2 PM on a Tuesday.
One more field note: if your company routes everything through an internal NTP server instead of the public pool, point chrony at that internal server and make sure nothing between you and it is dropping UDP 123. Nine times out of ten, reach = ? on a corporate network is an endpoint firewall silently killing NTP. And while you’re building out your ops toolkit, our guides on analyzing Linux logs with journalctl and troubleshooting high load on Linux servers pair nicely with this one.
Q: Why chrony instead of ntpd or systemd-timesyncd?
Because modern servers are usually VMs, and VMs do weird things to clocks — they pause them, migrate them, inflate them. Chrony detects and corrects for that behavior, syncs fast at boot thanks to iburst, and stays stable on imperfect network links. systemd-timesyncd is fine for a laptop, ntpd is fine for old-school physical broadcast networks, but for a production VM or container, chrony is the balanced pick.
Q: I added makestep but the clock still won’t step. Why?
makestep only applies to the first N updates — the 3 in makestep 1 3. If your box has been up for a week and the offset shows up on update number 500, chrony will slew, not step. Restart chronyd to reset the counter, or force it manually with sudo chronyc makestep.
Q: It says synchronized, but the clock is still a second or two off the NTP server. Normal?
Yes. Network latency guarantees a small offset — a few milliseconds is healthy. If it’s consistently off by whole seconds, look at your network path to the NTP server; asymmetric routing or a firewall that queues packets is the usual suspect.
Q: Can I run chrony and ntpd at the same time for redundancy?
Please don’t. Two daemons both steering the system clock will fight each other and make the offset worse, not better. One daemon, many sources. Configure all your time servers as sources inside the single daemon instead.
Alright, that’s the whole playbook. Fixing Linux time sync with chrony and NTP is one of those problems where a tiny config change makes a huge difference — and honestly, I love that about it. Your logs will finally make sense, your certs will stop crying, your cron will fire when it should, and your replicas will get along again. We also have a server monitoring basics guide if you want to catch these problems before they bite.
If you’ve got a favorite time-sync trick or a horror story from a drifting clock, drop it in the comments — I’m always down to learn a better way. And if this saved you an afternoon, share it with the next engineer who’s about to lose theirs. Now go make sure your clocks are straight. Gas!