notes

working notes on backend, infra, and the occasional yak shave.

← back

systemd timers I keep reaching for

2026-03-03 · ~3 min read

Cron is fine. It has been fine for decades. But once a host runs more than a few periodic jobs, the trade-off shifts: cron output disappears into mail (or /dev/null), failures are silent, and missed runs after a reboot are lost forever. systemd timers fix all three.

the minimal pair

A timer is two units: a .service describing the work, and a .timer describing when. For a nightly database backup:

# /etc/systemd/system/db-backup.service
[Unit]
Description=Nightly Postgres backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-pg.sh
User=postgres
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run nightly Postgres backup

[Timer]
OnCalendar=*-*-* 03:17:00
RandomizedDelaySec=10m
Persistent=true

[Install]
WantedBy=timers.target

Three things to notice. OnCalendar uses a systemd.time(7) spec — no cron syntax to mis-remember. RandomizedDelaySec jitters the start time, which matters once you have a fleet. Persistent=true means a missed run (e.g. host was down at 03:17) will execute on next boot.

observability you get for free

  • systemctl status db-backup.timer shows the next firing time.
  • systemctl list-timers lists all timers, sorted by next-run.
  • journalctl -u db-backup.service gives you logs, scoped to that unit, with timestamps.
  • If the service exits non-zero, you get a failure state. You can wire that to an OnFailure= handler — typically another oneshot that pages you.

when I still reach for cron

Two cases. First, environments that aren't systemd — Alpine containers, BSDs, anything restricted. Second, a single ad-hoc cleanup script that nobody is going to maintain. The friction of two unit files isn't worth it for a one-line job.

Everything else: timer.