Cron vs systemd timers: how to schedule jobs on Linux

Schedule a recurring job both ways, with a crontab line and with a systemd timer, and pick the right one for logging, missed runs and random delays.

Tested on Ubuntu 24.04 LTS, Debian 13 Stable, AlmaLinux 10

Before you start

  • A Linux server with systemd
  • Root or a user with sudo
  • A script or command you want to run on a schedule

Use cron for a quick one-line job: crontab -e, add 30 3 * * * /usr/local/bin/backup-db.sh, done. Use a systemd timer when you want the job's output in the journal, runs that were missed while the server was off made up at boot, a random delay, or protection against two runs overlapping; it takes two small files instead of one line.

Both are standard on every current distribution, and nothing stops you using both on one server. This tutorial schedules the same nightly job each way, so you can compare them. It was run on Ubuntu 24.04, Debian 13 and AlmaLinux 10.

The comparison

cron systemd timer
Setup One line in a crontab A .service file and a .timer file
Logs Wherever you redirect them; otherwise lost (or mailed, if mail is set up) In the journal: journalctl -u job.service
Missed runs (server off at 03:30) Skipped Run at the next boot with Persistent=true
Overlapping runs Possible, unless you add flock Never: a running service isn't started twice
Random delay across many servers Do it yourself with sleep RandomizedDelaySec=
Resource limits, sandboxing No Every systemd option (MemoryMax=, Nice=, ProtectSystem=)
Testing a schedule Guesswork, or crontab.guru systemd-analyze calendar prints the next runs
Run it now by hand Run the command yourself systemctl start job.service, same environment as scheduled

The example job is a script, /usr/local/bin/backup-db.sh, run every night at 03:30. Make your script executable (chmod 755) and give it a #!/bin/sh or #!/bin/bash first line.

Schedule it with cron

Install cron if crontab isn't found: the package is cron on Ubuntu and Debian, cronie on AlmaLinux and Rocky Linux.

bash
sudo apt install -y cron                                          # Ubuntu, Debian
sudo dnf install -y cronie && sudo systemctl enable --now crond   # AlmaLinux, Rocky

Edit root's crontab:

bash
sudo crontab -e

Add one line, and redirect the output so you can see what happened:

text
30 3 * * * /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1

The five fields are minute, hour, day of month, month and day of week. Some common ones:

Schedule Fields
Every 15 minutes */15 * * * *
Every hour, on the hour 0 * * * *
Every day at 03:30 30 3 * * *
Weekdays at 09:00 0 9 * * 1-5
First of the month at midnight 0 0 1 * *

The server's time zone decides what 03:30 means; check it with timedatectl (see set the time zone and NTP).

For a job that belongs to the system rather than a person, a file in /etc/cron.d/ is easier to manage with configuration tools. Those files have an extra field, the user to run as:

text
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 3 * * * root /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1

To stop two runs overlapping when one can take longer than the interval, wrap the command in flock:

text
*/15 * * * * flock -n /run/sync.lock /usr/local/bin/sync.sh

Check it worked

bash
sudo crontab -l
journalctl -u cron -n 20      # crond on AlmaLinux and Rocky

After the scheduled time, the journal shows a CMD (/usr/local/bin/backup-db.sh ...) line and your log file has the script's output.

Schedule it with a systemd timer

A timer starts a service. First the service, /etc/systemd/system/backup-db.service:

ini
[Unit]
Description=Back up the database

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

Type=oneshot means "run to completion", which is right for a job. There's no [Install] section: the timer starts it, not the boot. To run it as another user, add User= as in writing a systemd service.

Then the timer, with the same name and .timer, /etc/systemd/system/backup-db.timer:

ini
[Unit]
Description=Back up the database every night

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=10min

[Install]
WantedBy=timers.target

Persistent=true makes up a run that was missed while the server was off, once it boots. RandomizedDelaySec=10min starts the job at a random moment up to 10 minutes after 03:30, which spreads the load if many servers share the schedule (leave it out if the exact time matters).

Load and start the timer, not the service:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now backup-db.timer

Check it worked

bash
systemctl list-timers backup-db.timer

It shows the next run under NEXT, and after the first run, the last one under LAST. To run the job now, exactly as the timer would:

bash
sudo systemctl start backup-db.service
journalctl -u backup-db.service -n 20

The journal shows the script's output, then Finished backup-db.service. A failure shows up in systemctl --failed too.

Write and test OnCalendar schedules

OnCalendar= reads as DayOfWeek Year-Month-Day Hour:Minute:Second, with * for "any". Shortcuts exist: hourly, daily, weekly, monthly. Ask systemd when a schedule will fire before you use it:

bash
systemd-analyze calendar 'Mon..Fri *-*-* 09:00' --iterations=2
text
  Original form: Mon..Fri *-*-* 09:00
Normalized form: Mon..Fri *-*-* 09:00:00
    Next elapse: Thu 2026-10-22 09:00:00 UTC
       From now: 9h left
   Iteration #2: Fri 2026-10-23 09:00:00 UTC
Schedule OnCalendar
Every 15 minutes *:0/15
Every hour hourly (*-*-* *:00:00)
Every day at 03:30 *-*-* 03:30:00
Weekdays at 09:00 Mon..Fri *-*-* 09:00
First of the month monthly (*-*-01 00:00:00)

Note that daily means midnight, and weekly means Monday at midnight.

For "every so often after boot" instead of a clock time, use OnBootSec=5min with OnUnitActiveSec=1h.

Run something once, later

For a one-off, systemd-run makes a temporary timer without any files:

bash
sudo systemd-run --on-active=30min /usr/local/bin/backup-db.sh
sudo systemd-run --on-calendar='*:0/15' --unit=check-disk /usr/bin/df -h /

The first runs once in 30 minutes. The second repeats until the next reboot or sudo systemctl stop check-disk.timer. Both show up in systemctl list-timers.

Troubleshooting

A cron job works by hand but not from cron. Cron runs with a tiny PATH and no shell profile. Use full paths in the script (/usr/bin/pg_dump, not pg_dump), or set PATH= at the top of the crontab, and redirect output to a file so you see the error.

% in a crontab line breaks the command. Cron turns % into a newline. Escape it as \%, for example date +\%F.

The timer isn't in systemctl list-timers, or has no NEXT time. It isn't started, or the schedule never matches. (list-timers --all also shows inactive ones.) sudo systemctl enable --now backup-db.timer, and check the schedule with systemd-analyze calendar.

Failed to start backup-db.service: Unit backup-db.service has a bad unit file setting. A typo in the service file. systemd-analyze verify /etc/systemd/system/backup-db.service names the line.

Next steps

Doing it on VPSNine

Cron and systemd timers work the same on every VPSNine image, including Ubuntu 24.04, Debian 13 and AlmaLinux 10. Server time is UTC unless you change it. For whole-disk copies on a schedule without any script, use automatic snapshots in the panel. Compare plans.

Be first when orders open

Join the waitlist and we'll send you one email when VPSNine launches. Pick a plan if you already know which one you want, and we'll size the first servers around it.

  • One email, on launch day
  • Monthly or yearly, no lock-in
  • 14-day money-back guarantee