Timers and operations
Handle missed and randomized runs
Choose what happens after downtime and avoid making every server start maintenance at the same instant.
A cron job scheduled while the machine was off never happens. The machine comes back, cron looks at the clock, and the 3 AM slot is gone. Timers let you choose the behavior instead of inheriting it.
Catching up with Persistent=
Persistent=true lets a calendar timer catch up after downtime. systemd records the last trigger time on disk. If the machine boots and a run was missed, the timer fires once, immediately.
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=30min
That behavior is useful only when a late run is safe. A missed backup should absolutely catch up. A billing job that charges customers should not fire as a surprise the moment a machine comes back.
Think about incident recovery. A server was down for six hours, you bring it up, and the first thing it does is run every job it missed. That is the last thing you want while you are still checking the disk.
This is why idempotent jobs are safer. Idempotent means running the job twice has the same effect as running it once. A backup that overwrites today’s file is idempotent. A job that appends charges to invoices is not, and it needs a run-once guard before you let a timer catch it up.
Spreading the load
RandomizedDelaySec= spreads work across a window. With the 30 minute value above, each server picks a random delay after 03:00. A fleet of a hundred machines then does not hit the backup target at the same instant.
AccuracySec= controls scheduling precision. It defaults to one minute, because systemd batches timer wakeups to save power. If a job must fire at exactly 03:00:00, set AccuracySec=1s. Most jobs do not care, and the default is fine.
Verify the resulting schedule instead of trusting your reading of it:
systemctl list-timers backup.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-08-04 03:11:42 UTC 9h left Mon 2026-08-03 03:24:17 UTC 14h ago backup.timer backup.service
Notice the NEXT column. It says 03:11:42, not 03:00:00. That is the randomized offset, visible right there. The LAST column shows yesterday fired at 03:24:17, a different offset. Each elapse rolls a new one.
Decide per job
Let’s work through three common jobs: a temp-file cleanup, a database backup, and a monthly billing run.
Cleanup gets Persistent=true. Running it late costs nothing. Backup gets Persistent=true too, because a late backup beats no backup. Both get a RandomizedDelaySec= if they run on more than one machine.
Billing gets Persistent=false. A missed run should raise an alert, and a human decides when to run it. Automating that decision is how you charge someone twice.
Try this with the timers you already have. For each one ask: if this fired six hours late, right after a crash, would I be happy? Set Persistent= from the answer.
Lesson completed