12 October 2026

systemd timers vs cron: schedule a job on a VPS and see whether it actually ran

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.service says what to run, name.timer says when. Enable it with sudo systemctl enable --now name.timer. systemctl list-timers shows the next run, journalctl -u name.service shows what the job printed. Persistent=true runs a missed job after downtime, RandomizedDelaySec spreads the load. Cron is still fine for a one-liner you do not need to track.

What is wrong with cron on a server

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:

  • No memory of what happened. Output goes to local mail, and without a mail server it is simply lost. You add >> /var/log/job.log 2>&1 and hope.
  • No catch-up. If the server was off or rebooting at 03:30, the run is skipped. Classic cron does not retry it later (anacron exists for that, but it is a separate tool).
  • A bare environment. Cron starts jobs with a minimal PATH and none of your shell setup, so a script that works in the terminal fails with "command not found" at night.
  • No simple way to ask "when is the next run". You read the table and count in your head.

A systemd timer fixes each of these without extra tools, because systemd is already running as process one on your server.

Step 1. Write the service: what to run

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.

Step 2. Write the timer: when to run

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.

Step 3. Enable it and check the next run

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.timer

It prints nothing when everything is fine. Now reload and enable:

sudo systemctl daemon-reload && sudo systemctl enable --now backup.timer

Ask 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.

Step 4. Run it once by hand and read the log

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 or timer: how to choose

cron

systemd timer

Setup

one line in crontab -e

two files and one command

Log

you redirect output yourself

in the journal by default

Missed run after downtime

skipped

runs on boot with Persistent=true

Next run time

count by hand

systemctl list-timers

Environment

minimal PATH

explicit via Environment= or EnvironmentFile=

Overlapping runs

a second copy starts, flock helps

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.

To remove a timer

Disable and delete the files, then reload:

sudo systemctl disable --now backup.timer && sudo rm /etc/systemd/system/backup.{service,timer} && sudo systemctl daemon-reload

FAQ

Is a systemd timer better than cron?

For 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.

How do I run a timer every 15 minutes?

Use OnCalendar=*:0/15 for "every quarter hour on the clock", or OnBootSec=5min with OnUnitActiveSec=15min for "15 minutes after the previous run".

Why did my timer not 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.

What does Persistent=true do?

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.

Can I keep my crontab and add timers?

Yes, they live side by side. Move over the jobs where you want logs and catch-up, leave the rest.

Summary

  • A timer is a .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.
  • Output lands in journalctl -u name.service, no log redirects needed.
  • Persistent=true saves a run missed during downtime, RandomizedDelaySec spreads load across servers.
  • Important jobs go to timers, throwaway one-liners can stay in cron.
PUBLISHED
12 October 2026
AUTHOR
HIP-HOSTING
LANGUAGES
EN · RU