Scheduled work

A Timer Without a Receipt Is a Hope, Not a Schedule

Scheduled work is proven by the calendar expression, the trigger pair, the journal record, and the effect on disk — not by the timer being enabled.

The calendar and the trigger are two units

A systemd schedule is a pair: the .timer decides when, the .service decides what. Enabling only one of them, or naming them differently, is the classic silent failure. Persistent=true additionally runs a missed job at the next boot instead of skipping it, which matters for backups on hosts that are not always on.

# /etc/systemd/system/backup-example.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10m

[Install]
WantedBy=timers.target

RandomizedDelaySec spreads load so that every host in a fleet does not hit the same backend at exactly 03:00. Set an AccuracySec explicitly if you need the opposite: coalesced, precise firing.

Test the expression before trusting it

Calendar expressions are easy to write and easy to get wrong — an empty OnCalendar in the merged configuration means the timer never fires. Expand the expression and see the next executions before enabling anything.

systemd-analyze calendar '*-*-* 03:00:00'
systemctl cat backup-example.timer
systemd-analyze verify backup-example.timer

systemd-analyze calendar prints both the normalized form and the next few elapse times with the accuracy window. This is also where a typo such as Sun *-*-* 03:00 versus the intended daily schedule becomes visible immediately, instead of a week later as a missing backup.

The journal is the receipt

systemctl list-timers is the one-line audit: NEXT, LAST, and ACTIVATES. LAST is the receipt that the trigger fired; a timer whose LAST is older than its schedule claims is not running the job, whatever its enabled state says.

systemctl list-timers --all | grep backup-example
journalctl -u backup-example.service --since yesterday --no-pager
systemctl status backup-example.service

Read the service journal, not just the timer row: the timer can fire perfectly while the service fails in two seconds. For long-running jobs, record start and end in the script itself, so the journal shows duration, not only invocation.

Verify the effect, not the exit code

An exit code of zero is the job's opinion of itself. A backup job is healthy when a recent artifact exists, has the expected size and permissions, and passes whatever consistency check the format supports. Check the effect on disk, and alert on its age.

ls -l --time-style=long-iso /var/backups/example/ | tail -5
find /var/backups/example -mtime -1 -size +1M | grep -q . 
systemctl show backup-example.service -p Result -p ExecMainStatus

Run the service once by hand before enabling the timer, and give failures a place to go: an OnFailure= unit, or a monitoring check on artifact age, so a broken schedule is noticed by design rather than by incident.

Timer checklist

  • Ship the timer and service as a correctly named pair.
  • Decide Persistent deliberately for hosts that power off.
  • Expand the calendar expression before enabling.
  • Check LAST in list-timers after the first due window.
  • Read the service journal for failures shorter than a blink.
  • Verify the artifact on disk and alert on its age.
  • Wire an OnFailure hook or an external check.