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.targetRandomizedDelaySec 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.timersystemd-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.serviceRead 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 ExecMainStatusRun 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
Persistentdeliberately for hosts that power off. - Expand the calendar expression before enabling.
- Check
LASTinlist-timersafter 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
OnFailurehook or an external check.