Cron vs systemd timers
A timer costs two files instead of one line. Here is the table first, because for most jobs the answer is still cron and you can stop reading.
By the Withuse team · Updated
| Situation | Use |
|---|---|
| Fixed schedule, host always on, output to a log | cron |
| Portability across Unixes matters | cron |
| Whoever maintains it should not need unit syntax | cron |
| Interval that does not divide an hour | timer |
| Host reboots, sleeps or scales down | timer |
| Job needs dependencies or resource limits | timer |
| You want to ask when it next runs | timer |
Method note: the cron behaviour below was computed with the parser on this site. systemd is not available on the machine these guides are written on, so the timer syntax and semantics follow the systemd.timer and systemd.time manual pages — no output is quoted as observed.
The same job, both ways
# cron — one line 0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 # systemd — two files # /etc/systemd/system/backup.service [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh # /etc/systemd/system/backup.timer [Timer] OnCalendar=*-*-* 03:00:00 Persistent=true [Install] WantedBy=timers.target
For this job the crontab line is plainly better — shorter, portable, and obvious to anyone who opens it. The timer earns its extra weight only when you need one of the four things below, and "it is the modern way" is not one of them.
1. Intervals cron cannot express at all
# "every 90 minutes" in cron: no such expression
*/45 * * * * from 00:50 -> 01:00 | 01:45 | 02:00 | 02:45
└─45m─┘ └15m┘ └─45m─┘
# systemd
OnUnitActiveSec=90minA step divides a field's range, and the minute range restarts every hour, so no cron expression spans more than sixty minutes. We measured */45 producing alternating gaps of 45 and 15 minutes rather than the steady 45 people expect — the detail is in cron every 5 minutes.
OnUnitActiveSec measures from the previous run rather than against a clock face, so the interval is exactly what you wrote. This is the one capability that is not a matter of convenience: cron genuinely cannot say it.
2. Runs missed while the machine was off
Persistent=true records when the timer last fired and runs a missed occurrence as soon as the machine is available again. Cron has no equivalent — an occurrence that passes while the host is powered off is simply gone, which is why anacron exists as a separate tool for laptops and intermittently-running machines.
On a server that stays up this rarely matters. On anything that reboots, sleeps or scales to zero it is the difference between a nightly job running and quietly not running, and a silently skipped job is indistinguishable from one that never existed until somebody goes looking.
3. Output you can query
systemctl list-timers # next run, time remaining, last run journalctl -u backup.service # output and exit status of past runs
Cron's default is to email output, which on most modern servers goes nowhere at all — so people redirect to a file and then have to remember which file. And nothing in cron will tell you when a job runs next; you read the expression and work it out yourself, which is why tools that compute run times exist.
This is the difference most people notice first after switching, ahead of any scheduling feature, because it changes how you debug rather than how you schedule — and you debug far more often than you reschedule.
4. It is a service, with everything that implies
A timer triggers a service unit, so the job inherits the rest of systemd's vocabulary: After= to wait for a database, MemoryMax= to bound it, User= without a su wrapper, a restart policy. In cron every one of those has to be rebuilt inside the script, where it is invisible to anyone inspecting the schedule.
One thing both get wrong the same way
Daylight saving bites both. OnCalendar accepts an explicit timezone the way CRON_TZ does, and the same advice applies to each: keep schedules out of the 01:00–03:00 local window, where an hour either repeats or does not exist.
Two questions worth answering
Why can't cron express every 90 minutes?
Because a step divides a field's range, and the minute range restarts every hour, so nothing spans more than sixty minutes. The nearest approximation is wrong in a way that is easy to miss: */45 fires at :00 and :45, producing alternating gaps of 45 and 15 minutes rather than a steady 45. We computed that with the parser on this site. A systemd timer states the period directly with OnUnitActiveSec=90min and measures from the previous run rather than against a clock face, so the gap is the number you wrote.
Is cron still the right choice for anything?
For most simple recurring jobs on a machine that stays on, yes. A crontab line is one line, editable with crontab -e, portable to any Unix, and understood by everyone who will ever read it. Reach for a timer when you need one of the four capabilities above — not by default, and not because timers are newer.
References
If you landed here to check what an expression does, the parser will read it back to you in words. Crontab syntax covers the fields themselves. More tools at withuse.io/tools.