Replace a Cron Job with a systemd Timer
Why and how to migrate a cron entry to a systemd timer for better logging, dependency handling, and missed-run recovery.
Cron has been the default Linux scheduler for four decades for good reason — the syntax is tight and the daemon is practically bulletproof. But once a job starts mattering (it touches production data, it needs to run even if the machine was asleep, it has to wait for the network), cron's silence becomes a liability.
systemd timers cover the same ground with better tooling. This tutorial migrates a single cron job to a timer and shows what you gain in the process.
The cron job we're replacing
A nightly backup at 2:00 AM:
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
It works. But when it fails you have to remember to check /var/log/backup.log. If the machine was off at 2:00 AM, the job is just skipped. If the backup server is still booting at 2:00 AM, the job fails and nothing retries.
Step 1 — Write the service unit
A timer needs a service to activate. Create /etc/systemd/system/backup.service:
[Unit]
Description=Nightly backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Nice=10
IOSchedulingClass=idle
A few notes:
Type=oneshot— the unit runs to completion, then exits. This is what you want for a scheduled task, notType=simple.Wants=network-online.targetplusAfter=network-online.target— systemd will wait for the network to actually be reachable before starting. Cron does not do this.Nice=10andIOSchedulingClass=idle— the backup yields to interactive processes. Harder to set up cleanly in cron.User=backup— runs as a dedicated user. No moresudo crontab -ejuggling.
Step 2 — Write the timer unit
Create /etc/systemd/system/backup.timer:
[Unit]
Description=Run nightly backup at 02:00
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=10min
[Install]
WantedBy=timers.target
The important lines:
OnCalendar=*-*-* 02:00:00— every day at 2:00 AM. The calendar syntax is closer to English than cron's five-field dialect;systemd-analyze calendar '*-*-* 02:00:00'will show you the next few fire times.Persistent=true— if the machine was off at 2:00 AM, systemd runs the job the next time the machine boots. This alone is worth the migration for laptops or occasionally-offline servers.RandomizedDelaySec=10min— spreads the start time across a 10-minute window. Useful when many machines run the same job and you do not want them all hammering the backup server at once.
The timer has the same base name as the service (backup.timer and backup.service). That pairing is what links them — no need to declare it anywhere.
Step 3 — Enable and start
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
--now both enables the timer at boot and starts it immediately. Without it, the timer would only become active on next reboot.
Remove the old cron entry:
crontab -e
Delete the line and save.
Step 4 — Check that it is scheduled
systemctl list-timers backup.timer
You should see something like:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Wed 2026-04-24 02:07:33 UTC 14h left - - backup.timer backup.service
NEXT reflects the randomized delay — here it fell on 02:07 rather than exactly 02:00. Good.
Step 5 — Trigger it manually
You do not have to wait until 2:00 AM to know if the service runs. Start it by hand:
sudo systemctl start backup.service
Check the logs:
journalctl -u backup.service -n 100
This is the biggest quality-of-life win over cron. No more digging through /var/log/syslog or worrying about redirecting stderr. Every line the backup script writes to stdout or stderr lands in the journal, already tied to the unit and timestamped.
Follow live runs with:
journalctl -u backup.service -f
Step 6 — Handle failure
Cron silently emails root (if MTA is configured) and moves on. systemd has more options.
Create /etc/systemd/system/backup.service.d/on-failure.conf:
[Unit]
OnFailure=notify-failure@%n.service
Then write a notify-failure@.service that pages you via whatever channel you prefer (Slack webhook, email, ntfy, a small script). Because the failing unit's name is passed in as %i, a single notifier can cover every timer-backed service on the machine.
If you just want automatic retry, add to the service [Service] section:
Restart=on-failure
RestartSec=5min
Combined with Type=oneshot, systemd will retry the script up to the limit set by StartLimitBurst.
Converting cron syntax to OnCalendar
A rough map for common patterns:
| Cron | OnCalendar |
|---|---|
0 * * * * |
hourly |
0 0 * * * |
daily |
0 0 * * 0 |
weekly |
0 0 1 * * |
monthly |
*/15 * * * * |
*:0/15 |
0 9 * * 1-5 |
Mon..Fri 09:00 |
0 2,14 * * * |
02,14:00 |
Anything more exotic — run it through systemd-analyze calendar "..." to confirm it parses and to see the next fire times.
When to stick with cron
Not every job needs the upgrade. If you have a handful of no-stakes scripts that cron has been running correctly for years, leave them alone. The timer pattern pays off for jobs where failure matters, where retries matter, or where you have gotten tired of figuring out why the script works interactively but not under cron.
SysEmperor