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.
sudo apt install -y cron # Ubuntu, Debian
sudo dnf install -y cronie && sudo systemctl enable --now crond # AlmaLinux, RockyEdit root's crontab:
sudo crontab -eAdd one line, and redirect the output so you can see what happened:
30 3 * * * /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1The 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:
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>&1To stop two runs overlapping when one can take longer than the interval, wrap the command in flock:
*/15 * * * * flock -n /run/sync.lock /usr/local/bin/sync.shCheck it worked
sudo crontab -l
journalctl -u cron -n 20 # crond on AlmaLinux and RockyAfter 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:
[Unit]
Description=Back up the database
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-db.shType=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:
[Unit]
Description=Back up the database every night
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=10min
[Install]
WantedBy=timers.targetPersistent=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:
sudo systemctl daemon-reload
sudo systemctl enable --now backup-db.timerCheck it worked
systemctl list-timers backup-db.timerIt 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:
sudo systemctl start backup-db.service
journalctl -u backup-db.service -n 20The 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:
systemd-analyze calendar 'Mon..Fri *-*-* 09:00' --iterations=2 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:
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
- Read the job's output with journalctl.
- Decide what's worth backing up and where to: backups you should run.
- Turn a long-running app into a service: write a systemd service.
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.