A cron job silently skipped last night and nobody noticed. Set up a systemd timer instead: logs, catch-up after downtime, a clear next-run time.
The classic story: a nightly backup sits in cron, everyone trusts it, and three weeks later you find it has not run since the server rebooted at 03:29. Cron works, but it tells you almost nothing. A systemd timer does the same job and adds a log, a clear "next run" time and a catch-up after downtime. Here is a working pair of files and what to check after you enable it.
In short. A timer is two small files:
name.servicesays what to run,name.timersays when. Enable it withsudo systemctl enable --now name.timer.systemctl list-timersshows the next run,journalctl -u name.serviceshows what the job printed.Persistent=trueruns a missed job after downtime,RandomizedDelaySecspreads the load. Cron is still fine for a one-liner you do not need to track.
Cron is a scheduler from the 1970s: one line in a table, five fields for time, then a command. It is short and everyone knows it, which is why people keep using it. The trouble starts when the job is important:
>> /var/log/job.log 2>&1 and hope.anacron exists for that, but it is a separate tool).PATH and none of your shell setup, so a script that works in the terminal fails with "command not found" at night.A systemd timer fixes each of these without extra tools, because systemd is already running as process one on your server.
First the .service file. It describes one job, not a daemon: start the script, wait until it exits, done. Type=oneshot tells systemd exactly that. Put it in /etc/systemd/system/:
Use the full path in ExecStart: systemd does not search your PATH the way a shell does. Make the script executable first with sudo chmod +x /usr/local/bin/backup.sh. Anything the script prints with echo goes to the journal automatically, so no log redirects are needed.
The .timer file must have the same name as the service (backup.timer triggers backup.service). This one runs every night at 03:30:
What each line does:
OnCalendar is the schedule in the form weekday year-month-day hour:minute:second. Stars mean "any", so *-*-* 03:30:00 is every day at 03:30. Time uses the server time zone.RandomizedDelaySec=300 shifts each run by a random delay of up to five minutes. If you run many servers, they will not all hit your storage at the same second.Persistent=true remembers the last run on disk. If the server was off at 03:30, the job runs right after boot.WantedBy=timers.target makes the timer start after every reboot once enabled.Not sure your schedule means what you think? Ask systemd to translate it before you enable anything:
This is the feature cron never had: you see the real next moment, not the five fields. The shorthand names also work, for example daily, weekly and hourly.
Check the files for typos, tell systemd to re-read them and enable the timer. Note that we enable the .timer, not the service:
sudo systemd-analyze verify /etc/systemd/system/backup.service /etc/systemd/system/backup.timerIt prints nothing when everything is fine. Now reload and enable:
sudo systemctl daemon-reload && sudo systemctl enable --now backup.timerAsk for the schedule:
One line, and it answers the question cron could not: the next run is tomorrow at 03:31:44. The extra minute and a half is the random delay from RandomizedDelaySec. The LAST column is empty because the job has not run yet.
Do not wait until night to find a typo. Start the service directly. It does not change the schedule:
This is the log cron never gave you: start time, everything the script printed, and the verdict. Deactivated successfully means exit code 0. If the script fails you get Failed with result 'exit-code' in the same place, and systemctl status backup.service shows the exit status.
cron | systemd timer | |
|---|---|---|
Setup | one line in | two files and one command |
Log | you redirect output yourself | in the journal by default |
Missed run after downtime | skipped | runs on boot with |
Next run time | count by hand |
|
Environment | minimal | explicit via |
Overlapping runs | a second copy starts, | by default a running unit is not started twice |
Works without systemd | yes | no |
Our rule of thumb: if the job matters (backups, certificate renewal, cleanup that frees disk), use a timer. If it is a throwaway one-liner like "ping this URL every five minutes", cron is fine and faster to write. Many system jobs on Ubuntu itself already moved to timers: systemctl list-timers on a fresh server shows apt updates, log rotation and certificate renewal.
Disable and delete the files, then reload:
sudo systemctl disable --now backup.timer && sudo rm /etc/systemd/system/backup.{service,timer} && sudo systemctl daemon-reloadFor jobs you care about, yes: it keeps a log, shows the next run and can catch up after downtime. For a quick throwaway task cron is simpler.
Use OnCalendar=*:0/15 for "every quarter hour on the clock", or OnBootSec=5min with OnUnitActiveSec=15min for "15 minutes after the previous run".
Check that you enabled the .timer, not the .service, that systemctl list-timers lists it, and that journalctl -u name.service has no errors. Also check the server time zone: OnCalendar uses it.
It stores the time of the last run on disk. If a scheduled moment passed while the server was off, the job runs right after the next boot.
Yes, they live side by side. Move over the jobs where you want logs and catch-up, leave the rest.
.service (what to run, Type=oneshot) plus a .timer (when to run), enabled with systemctl enable --now name.timer.systemd-analyze calendar shows when a schedule will really fire, systemctl list-timers shows what is planned.journalctl -u name.service, no log redirects needed.Persistent=true saves a run missed during downtime, RandomizedDelaySec spreads load across servers.