Systemd timers vs cron: when to choose what
A pragmatic decision guide for picking between systemd timers and cron, written from the trenches of running both for years.
- Linux
- systemd
- cron
- Operations
I have a soft spot for cron. It is two characters of syntax I have to look up every six months, but the file is editable with nano, the language is the same on every Linux box from 2003 to today, and when something fails it fails the way old Unix things fail: silently, in your inbox.
Systemd timers are the opposite. Verbose, structured, tightly integrated with the init system, beautifully observable. And the syntax is something you do not look up because the man page is right there.
After years of running both, here is when each one earns its place.
Use cron when
- The job is one line and the system is one box. A nightly database dump, a
/etc/letsencrypt/renewal-hooks/-style trigger. Two minutes to write, two minutes to forget about. - You need it to work the same on every distro for ten years. Cron is the lingua franca. If your script needs to run on a 2015 Debian, a 2024 Alpine, and a Synology NAS, cron is the lowest common denominator. systemd timers exist on most modern distros, but not all.
- Failure is okay or visible by some other means. If the cron job writes to a queue that has its own monitoring, and the failure mode is “the queue gets a bit behind, someone notices in 10 minutes”, cron is fine.
Use systemd timers when
- The job is part of a real service. Backups, log rotation, certificate rotation, queue consumers that wake up periodically. Anything where the timer is the trigger and the unit is the work, and the work might be a multi-minute process with dependencies, environment variables, namespaces, or restart policies. systemd’s unit model is a real model. Cron’s environment is a postcard.
- You want observability without inventing it.
systemctl list-timers,journalctl -u myjob.timer,systemctl status myjob.service. You see the last run, next run, exit code, output, all in one place. You do not need to ship logs anywhere; the journal is right there. - The job depends on other units. “Run after the network is up”, “run after the database service is healthy”, “do not run if the encrypted disk is not mounted” — these are one-line declarations in systemd, and they are nightmares in cron.
- You need cron’s behaviour, but with a randomised offset to avoid the thundering herd.
RandomizedDelaySecis the feature you wish cron had.
Three rules I now apply automatically
- If a job lives inside an application I deploy, it goes in systemd. It deploys with the app, monitors with the app, and when the app rolls back, the timer rolls back too.
- If a job is “the OS reminding me to do a small thing on this one server”, it goes in cron. Letsencrypt renewals, log directory cleanup, the kind of thing where the existence of the cronfile is the documentation.
- Whatever I pick, I write it in version control.
/etc/cron.d/myjoband/etc/systemd/system/myjob.timerare both files. They both go in the same Ansible/Salt/whatever repo. The boundary is “this server’s package”, not “this tool’s syntax”.
The unsexy truth: most teams make this decision on vibe, regret it, and then accumulate both for ten years anyway. Pick deliberately, run both where they earn their place, and stop pretending one of them is going away.
Was this useful?