A silently failing cron job is a decades-old sysadmin classic: cron runs the command at the scheduled time, and that’s it. No notification by default, no structured history, no retry mechanism. A systemd timer solves this by directly inheriting the reliability mechanisms already present for any systemd service.
What cron never does on its own
cron triggers a command at the scheduled time, without ever checking whether it succeeded. A failing job (non-zero exit code) triggers no retry, notifies nobody, and leaves at best a trace in the local system mail (MAILTO), an old convention rarely monitored on a modern server where local mail isn’t even configured.
# No retry, no alert, no structured history:
# if this command fails, nothing signals it
0 3 * * * /usr/local/bin/backup.sh
The systemd timer: the same execution, with history and retry included
A systemd timer triggers an ordinary service at the scheduled time, which means that service inherits everything systemd already offers: Restart=on-failure for automatic retry, a full history queryable via journalctl, and the same systemctl status integration to check the last run’s state.
# backup.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Restart=on-failure
RestartSec=300
# backup.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true: catching what cron ignores
A machine powered off or asleep at the moment a cron task should have run simply loses that execution: no native catch-up mechanism. Persistent=true on a systemd timer changes this: if the system was off at the scheduled time, the timer triggers the missed run at the next boot, a detail that matters particularly on a machine not running 24/7 (a development workstation, a VM stopped overnight to cut costs).
Checking the real execution history
journalctl gives a complete, structured history of every execution of the service triggered by the timer, unlike cron whose only trace, absent explicit configuration, is limited to local system mail.
# Full history of every service execution,
# successes and failures, with no extra configuration
journalctl -u backup.service --since "7 days ago"
# Next scheduled run and last actual run,
# information cron never gives natively
systemctl list-timers backup.timer
What cron keeps as a real advantage
cron stays simpler to read and edit for a trivial one-off task, with no separate .service file to maintain alongside the timer, and stays omnipresent on systems where systemd isn’t the init manager (some minimal containers, some BSDs). For a genuinely critical task where a silent failure has a real cost (a backup, a log purge that prevents disk saturation), switching to a systemd timer eliminates an entire class of incidents discovered too late, at the cost of slightly more verbose configuration.
Takeaway
A failing cron job notifies nobody by default, with no retry or structured history, a limitation that’s always existed and still regularly surprises people. A systemd timer, by triggering an ordinary service, directly inherits Restart=, the full history via journalctl, and Persistent=true to catch a missed run after a shutdown. The choice between the two mostly depends on the real cost of a silent failure: for a genuinely critical task, that visibility concretely changes how fast a problem gets detected, one of the habits separating an infrastructure that discovers its own incidents from one that discovers them by accident.