In short
For simple, non-critical tasks cron is perfectly fine. For jobs whose failure somebody needs to notice, such as backups, certificate renewal or Nextcloud background jobs, systemd timers are usually the better choice because status and logs are visible via systemctl and journalctl.
| Criterion | systemd timers | cron |
|---|---|---|
| Setup | Two units: .service and .timer | One line in the crontab |
| Schedule | OnCalendar=, e.g. *-*-* 03:00, or relative with OnBootSec= | Five fields, e.g. 0 3 * * * |
| Logging | Automatic in the journal, read with journalctl -u | Only via mail (MAILTO) or your own redirection |
| Missed runs | Persistent=true catches up after boot | Lost (unless you use anacron) |
| Overlap | If the service is still running, the timer does not start it again | Possible, guard with e.g. flock |
| Environment | Defined in the unit, reproducible | Minimal PATH, a common source of errors |
| Resources and security | CPU, memory and I/O limits, sandboxing through unit options | Nothing built in |
| Overview | systemctl list-timers shows last and next run | crontab -l per user, plus /etc/cron.d |
Pick systemd timers if …
- a failure must be noticed, for example through monitoring of failed units.
- the job runs long and must not overtake itself.
- the machine is not always on and missed runs should be caught up.
- you want to limit resources or run the job with reduced privileges.
Pick cron if …
- the task is short, simple and not critical.
- you look after systems without systemd, such as containers or older distributions.
- the application documents a crontab line and you want to stay close to the docs.
- users should maintain their own jobs without root access.
Pitfalls from operations
- Cron: the script works in a terminal but not from cron. Almost always
PATHor missing variables. Use absolute paths. - Cron: a
%in the command line is treated as a newline and must be written as\%, for example indate +%F. - systemd: after creating the units,
systemctl daemon-reloadandsystemctl enable --now name.timerare missing. The timer exists but never fires. - systemd: check
OnCalendarexpressions withsystemd-analyze calendarfirst, or the job runs at times you did not expect.
FAQ
Should I convert all cron jobs to timers?
No. Converting pays off for jobs whose failure matters or that you want to monitor. Simple cleanup jobs can stay in cron. What matters is that each task is defined in exactly one place.
How do I see whether a timer last succeeded?
systemctl list-timers shows the last run, systemctl status name.service the result and journalctl -u name.service the output. Failed runs also show up in systemctl --failed.