How to Use systemd Timers Instead of Cron (Logs, Retries and Catch-Up Runs)

systemd timers: cron that catches up — Logged, listed, and runs the job it missed.

Cron (tutorial 20) works and will keep working. But systemd timers fix cron's three irritations: output is logged where everything else is logged, a job missed because the server was off at 2 am runs when it comes back, and systemctl list-timers shows exactly when everything will next run. Two small files per job.

Quick reference

TaskWhat
The job/etc/systemd/system/backup.service
The schedule/etc/systemd/system/backup.timer
Enablesudo systemctl enable --now backup.timer
See all timerssystemctl list-timers
Run the job nowsudo systemctl start backup.service
Logsjournalctl -u backup.service
Test a schedule expressionsystemd-analyze calendar "Mon..Fri 07:30"

1. The service: what to run

sudo nano /etc/systemd/system/backup.service

[Unit]
Description=Nightly backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

oneshot means "run once and exit" — a script, not a daemon. No [Install] section is needed; the timer starts it.

2. The timer: when to run it

sudo nano /etc/systemd/system/backup.timer

[Unit]
Description=Run backup nightly at 02:00

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=5m

[Install]
WantedBy=timers.target

3. Enable and check

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers

The table shows NEXT, LEFT, LAST and PASSED for every timer on the system — including Ubuntu's own (apt, logrotate, fstrim).

Run it immediately to test, and read the output:

sudo systemctl start backup.service
journalctl -u backup.service -n 50

Everything the script printed is in the journal, with timestamps. No >> log 2>&1 needed.

4. Schedule expressions

OnCalendarMeaning
--* 02:00:00daily at 2 am
dailymidnight
hourly / weekly / monthlyon the hour / Monday 00:00 / 1st 00:00
Mon..Fri 07:30weekdays at 7:30
Sun 03:00Sundays at 3 am
*:0/15every 15 minutes
--01 00:00:00first of the month

Check any expression before trusting it:

systemd-analyze calendar "Mon..Fri 07:30"

It prints the next run time.

Alternatively, OnBootSec=5min plus OnUnitActiveSec=1h means "5 minutes after boot, then every hour" — handy for things like the DuckDNS update that just need to happen regularly.

5. Convert a cron line

0 4 * * 0 docker system prune -af

becomes a service with ExecStart=/usr/bin/docker system prune -af and a timer with OnCalendar=Sun 04:00. Full paths still apply (which docker).

6. Run as a normal user

mkdir -p ~/.config/systemd/user

Put the two files there, use systemctl --user enable --now name.timer, and run loginctl enable-linger $USER once so the timers run when you're not logged in. Useful for jobs that touch your own files without root.

Troubleshooting

Timer listed but job never runs. The service file has an error: systemctl status backup.service and journalctl -u backup.service.

Changed the files, nothing changed. sudo systemctl daemon-reload, then restart the timer.

Runs at the wrong time. The server's time zone (tutorial 9). OnCalendar can also take a zone: --* 02:00:00 Europe/London.

Persistent job ran twice after a long downtime. It doesn't — one catch-up run only. If it seems to, two timers point at the same service.

Wrapping up

A .service for the job, a .timer for the schedule, Persistent=true so missed runs catch up, list-timers to see the whole calendar and journalctl -u for the output. Keep cron for one-liners; move anything that matters to a timer.

Call us