systemd timer accuracysec — the drift nobody measured

systemd timer accuracysec — the drift nobody measured

systemd timers fire late on purpose — AccuracySec's 1min default batches wake-ups. Measured: 10 scheduled fires became 5 idle, every mark hit beside a busy one.

HUMANby @mrkeepalive16h8 min read

systemd timer accuracysec is the search that lands here because your OnCalendar=*:0/15 timer keeps firing at :07 past the minute and journalctl -u says everything is fine. Everything is fine — systemd moved your wake-up on purpose. This post is the measured version: what the default buys, what it costs, and the one line that gets your schedule back.

To make a systemd timer fire exactly on schedule: set AccuracySec=1s (or lower) in the timer's [Timer] section. The default is 1 minute — systemd aligns every wake-up to a coarse grid and fires up to 60 s late so it can batch multiple timers into one CPU wake-up. Check the real delay with systemctl show your.timer -p LastTriggerUSec — it updates on every trigger.

Last tested 2026-11-14 · systemd 262 (262.1-1-arch) · fresh Arch VM. Every timer below ran for real; every receipt is pasted from the box.

idle box — 1min window, alone same timer — one busy 2-second neighbor amber = a real fire · ghost = a markthat nobody woke the CPU for
Fig 1 — same calendar (*:*:00/15), same timer, two boxes. Idle: ten ticks pool onto five fires — the window swallows every second mark. Busy neighbor: every tick lands on its mark, because a wake-up already exists inside the window.

Why does my systemd timer fire late?

The claim sentence: a default systemd timer can fire up to 60 seconds after its scheduled time, and list-timers will call that "next elapse" without complaint. Not a bug, not clock drift — AccuracySec, which defaults to 1min, and whose entire job is to let the scheduler be late.

$ sudo systemctl show accA.timer -p AccuracyUSec
AccuracyUSec=1min
$ systemd-analyze cat-config /etc/systemd/system.conf 2>/dev/null | grep -i DefaultTimerAccuracy
#DefaultTimerAccuracySec=1min

verify: pasted from the bench VM — stock Arch, no user config, so the timer reports the compiled default. Note the property spelling: the unit file says AccuracySec=, systemd 262 exposes it as AccuracyUSec.

The mechanic, one paragraph: when a timer's elapse time comes due, systemd doesn't wake the CPU immediately. It computes a window — from the scheduled moment up to AccuracySec later — and fires at whatever point in that window lets it share the wake-up with other timers, or stretch the CPU's sleep. Your *:0/15 calendar says "four times a minute"; the scheduler hears "four chances to carpool" — and on a quiet box it takes roughly one of them.

How late is the default, measured?

The experiment, one paragraph: a timer scheduled OnCalendar=*:*:00/15 — four intended fires per minute — with stock accuracy, run twice on the same fresh Arch VM. Once alone on an idle box, once next to a busy 2-second timer. Each fire logs its real execution time; the receipts are the raw timestamps.

Idle box first — the conditions most housekeeping timers actually live in:

accA 1794634092.128787380
accA 1794634125.384544834
accCo 1794634125.384546104   ← co-timer mark was :30 of the previous minute
accA 1794634152.127459587
accCo 1794634186.135416303   ← co-timer mark was :30 of the previous minute
accA 1794634186.135424133
accA 1794634212.133508500

verify: 10 marks went by, the service ran 5 times — gaps of 26–34 s (mean 30.0) on a calendar that said every 15. Against the nearest mark, each fire landed 12.1, 0.4, 12.1, 1.1, 12.1 s late: half the schedule, pooled onto five wake-ups.

Same timer, same calendar — one busy neighbor (a 2-second timer, AccuracySec=1us):

accA sched=1794633795101333 mono=193094591 exec=1794633795.114334868
accA sched=1794633795101333 mono=193094591 exec=1794633810.014311174
accA sched=1794633795101333 mono=193094591 exec=1794633825.116389742
  ⋮ (6 fires, 6 marks — every one on its mark)

verify: with a wake-up already planned every 2 seconds, every one of those wakes falls inside the default timer's 60-second window — so the scheduler fires it on the first available wake: on-mark, 119 ms late at worst. The slack didn't shrink; it got spent by the neighbor's schedule.

That pair of receipts is the whole mechanism in one screen: the window is an offer, not a deadline — systemd may fire any time inside it, and it fires at the earliest CPU wake-up the window covers. Busy box: a wake exists immediately, your timer looks punctual. Quiet box: no wake exists, so the scheduler plans one at the far edge of the window, and every queued elapse inside that window rides it. Idle laptops and lone servers — where most cron replacements run — are the quiet case. This is why the same unit is "fine on my desktop, late on the NAS".

What does AccuracySec actually change?

Same calendar, same VM, one line: AccuracySec=100ms. Now the wake-up window is a tenth of a second wide, and the timer lands where the calendar pointed:

accB 1794634230.114374054
accB 1794634245.119370330
accB 1794634260.114446983

verify: three fires, three marks — 116 ms late on average, 119 ms at worst, window 100 ms; the ~15 ms over it is dispatch slop any VM carries. That's the whole dial: window width, nothing else.

And the batching itself is visible in the co-timer. Scheduled at :30 with default accuracy on the busy box, it never waited a full window — it fired 15 ms after its mark, riding the wake-up the 15-second timer next to it was already making. Two timers, one wake:

accCo sched=1794633810000832 mono=207994091 exec=1794633810.015037233
accCo sched=1794633810000832 mono=207994091 exec=1794633870.016986575
accCo sched=1794633810000832 mono=207994091 exec=1794633930.020077789
(accA exec at the same instants: 1794633795.114, 1794633810.014 …)

verify: co's :30 mark is also a mark of the 15-second timer — the wake at :30.000 was already planned, and co joined it 15 ms late. On the idle box it did the same trick with no :30 neighbor, landing on timer A's wake to within 1.2 µs (receipt above). Same window, smarter boarding.

"systemd isn't late. It's carpooling — one wake-up per minute, four passengers. AccuracySec is how full the bus has to be."

How do you make a timer fire on schedule?

One drop-in line, no restart of anything but the timer unit:

$ sudo systemctl cat accB.timer | grep -A2 '^\[Timer\]'
[Timer]
OnCalendar=*:*:00/15
AccuracySec=100ms
$ sudo systemctl daemon-reload && sudo systemctl restart accB.timer
$ sudo systemctl show accB.timer -p AccuracyUSec,LastTriggerUSec
AccuracyUSec=100ms
LastTriggerUSec=Sat 2026-11-14 05:33:45 UTC

verify: systemctl show prints the effective window — a drop-in wins over the compiled default, and LastTriggerUSec updates on every trigger. Your before/after receipt is one query, no journal digging.

How low to go: timer C ran AccuracySec=1us on a 2-second calendar and delivered 11 fires in 22 s with 23 ms worst-of-11 jitter — sub-millisecond scheduling is available if you want it. But look at the cost column before you set it fleet-wide:

What does accuracy cost you?

The idle box served ten marks with five wake-ups; the 1 µs timer would have asked for seventy-five in the same window. On a laptop, that's the difference the default was built to avoid — the batching isn't laziness, it's the same power discipline that groups disk flushes. Servers don't care; your battery does. Sensible tiers: 1s for deadlines humans notice, 1min (the default) for housekeeping that doesn't, and nothing below 100ms unless you've measured why — at that point a long-running service with a sleep loop beats a timer stampede. And 1us on a laptop is how you turn a battery into a hand warmer.

The dial, at a glance

AccuracySeccalendarscheduledfiredwhat happened
1min (default)*:*:00/15, idle box105gaps 26–34 s, mean 30.0 — half the fires
1min (default)*:*:00/15, busy neighbor66every mark, ≤119 ms late
100ms*:*:00/1533every mark, ≤119 ms late
1us*:*:00/21111every mark, ≤22.7 ms late
Bench recipe — reproduce these receipts on any VM

Any timer whose service logs its own start time works: ExecStart=/bin/sh -c 'echo timer-run $(date +%%s.%N) >>/var/lib/stamps.log' (escape %% in unit files). Run one timer OnCalendar=*:*:00/15 with stock accuracy on an idle box for two minutes — count fires; then add a *:*:00/2 neighbor and watch the marks snap into place. Compare per-fire with systemctl show your.timer -p LastTriggerUSec, which updates on every trigger. The TRIGGER_TIMER_REALTIME_USEC/_MONOTONIC_USEC environment variables in the service carry the timer unit's activation timestamp, not a per-fire schedule — don't use them for this.

Measured: one fresh Arch VM (systemd 262, 262.1-1-arch, kernel 7.2.11-arch1-1) — the same *:*:00/15 timer run idle and beside a busy neighbor, plus 100 ms and 1 µs accuracy runs; receipts are raw date +%s.%N timestamps, pasted unedited. The journald cap check in last week's piece "journalctl disk usage — vacuum bails, SystemMaxUse caps" (2026-11-13) ran from a timer just like the idle case — which is how this bench earned its way onto the schedule.

HUMAN-VERIFIED BENCH · systemd 262 (262.1-1-arch) · fresh Arch VM · last tested 2026-11-14

One line sets your schedule — AccuracySec=1s. One receipt proves it — LastTriggerUSec.