Replacing cron with systemd timers
Cron is one of the most reliable tools on a Linux box, and I don't say a word against it for simple cases. But three things kept biting me: I couldn't easily see a job's output, jobs that ran long could overlap with the next run, and when the machine was asleep at 3 a.m. the job simply didn't happen. systemd timers fix all three, at the cost of more boilerplate. On servers I now reach for them by default.
A timer is two files
Where cron is one line, a systemd timer is a .service (what to run) plus a .timer (when to run it). Here's a nightly database backup. The service first:
# /etc/systemd/system/db-backup.service
[Unit]
Description=Nightly database backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/db-backup.sh
Then the timer:
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run db-backup nightly
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Enable and start the timer (not the service):
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer
The three things that won me over
Persistent=true. If the machine was off when the job was due, systemd runs it once as soon as it's back. Cron just skips the missed run and waits for tomorrow. For a laptop or an occasionally-rebooted VM, this alone is the reason.
No overlap, for free. Because the job is a service, systemd won't start a second instance while the first is still running — the unit is already active. A cron job that overruns its interval will cheerfully start a second copy on top of the first. I've watched that turn a slow backup into a thundering herd.
Logs, by default. Everything the job writes to stdout and stderr lands in the journal, tagged by unit:
journalctl -u db-backup.service --since today
journalctl -u db-backup.service -n 50 --no-pager
No more wrapping the command in >> /var/log/mything.log 2>&1 and rotating it yourself. And because it's the journal, you get the exit status and timing for every run.
Reading the schedule
Two commands I use constantly. list-timers shows the next and last run for everything:
systemctl list-timers --all
And if I'm unsure a calendar expression means what I think, systemd-analyze will expand it to real timestamps:
$ systemd-analyze calendar "Mon *-*-* 09:00:00"
Normalized form: Mon *-*-* 09:00:00
Next elapse: Mon 2025-09-29 09:00:00 UTC
From now: 6 days left
That has saved me from more than one "wait, is that every Monday or the first of the month" mistake. For running something relative to boot or at an interval rather than a wall-clock time, OnBootSec= and OnUnitActiveSec= replace OnCalendar.
When I still use cron
If a box has no systemd (some containers, some minimal images), or the job is a genuinely throwaway one-liner on a machine I don't own, cron wins on effort. The two-file overhead is real. But for anything I maintain — anything where I'll eventually ask "did that run, and what did it say?" — the journal integration and the missed-run handling pay for the extra typing many times over.