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

Source: https://vpsnine.com/tutorials/cron-vs-systemd-timers · Updated: 2026-10-21

Difficulty: beginner · Time: about 20 minutes · Tested on: Ubuntu 24.04 LTS, Debian 13 Stable, AlmaLinux 10

Prerequisites:

- 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](https://vpsnine.com/tutorials/set-timezone-and-ntp-linux)).

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](https://vpsnine.com/tutorials/create-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

- Read the job's output with [journalctl](https://vpsnine.com/tutorials/read-logs-with-journalctl).
- Decide what's worth backing up and where to: [backups you should run](https://vpsnine.com/help/backups-you-should-run).
- Turn a long-running app into a service: [write a systemd service](https://vpsnine.com/tutorials/create-a-systemd-service).

## Doing it on VPSNine

Cron and systemd timers work the same on every VPSNine image, including [Ubuntu 24.04](https://vpsnine.com/os/ubuntu-24.04), [Debian 13](https://vpsnine.com/os/debian-13) and [AlmaLinux 10](https://vpsnine.com/os/almalinux-10). Server time is UTC unless you change it. For whole-disk copies on a schedule without any script, use [automatic snapshots](https://vpsnine.com/help/snapshots) in the panel. Compare [plans](https://vpsnine.com/plans).
