Cron vs systemd Timers: Scheduling Jobs on Linux

In short
  • cron is a one-line schedule in a crontab: quick, portable and familiar.
  • systemd timers need two small unit files but add logging in the journal, catch-up for missed runs, randomised delays and resource limits.
  • For a personal one-liner, cron is fine. For anything a server depends on, a timer is easier to monitor and debug.

Scheduling with cron

Each user has a crontab, edited with crontab -e (and listed with crontab -l). Each line is five time fields followed by a command:

# ┌ minute (0-59)
# │ ┌ hour (0-23)
# │ │ ┌ day of month (1-31)
# │ │ │ ┌ month (1-12)
# │ │ │ │ ┌ day of week (0-7, 0 and 7 = Sunday)
# │ │ │ │ │
  30 2 * * *   /home/alice/bin/backup-home.sh >> /home/alice/backup.log 2>&1
  */15 * * * * /usr/local/bin/check-queue
  0 9 * * 1-5  /usr/local/bin/morning-report
  @reboot      /home/alice/bin/start-tunnel.sh

System-wide jobs go in /etc/crontab or a file in /etc/cron.d/; those have an extra user field before the command. Scripts dropped into /etc/cron.daily/, cron.weekly/ and so on run at those intervals.

The usual cron pitfalls

  • Minimal environment. cron runs jobs with a short PATH (typically /usr/bin:/bin) and none of your shell's settings. Use absolute paths, or set PATH= at the top of the crontab.
  • % is special in a crontab line and must be escaped as \% — a classic trap with date +%F.
  • Output disappears. Without a working local mail setup, output and errors are lost. Redirect to a log file as above.
  • Missed runs stay missed. If the machine was off at 02:30, the job simply doesn't run (unless you install anacron).
  • Not always installed. Debian and Ubuntu install cron by default; Arch doesn't (the package is cronie), and minimal Fedora/RHEL images may need cronie too.

Scheduling with a systemd timer

A timer is a pair of units: a .service that says what to run and a .timer that says when. The same nightly backup as above, as a system-wide job:

# /etc/systemd/system/backup-home.service
[Unit]
Description=Back up /home/alice with rsync

[Service]
Type=oneshot
User=alice
ExecStart=/home/alice/bin/backup-home.sh
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/backup-home.timer
[Unit]
Description=Nightly home backup

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

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup-home.timer

# When does it run next, and when did it last run?
systemctl list-timers backup-home.timer

# Run it once right now, and read its output
sudo systemctl start backup-home.service
journalctl -u backup-home.service -n 50

What you gain over cron:

  • Persistent=true runs a missed job at the next boot if the machine was off at the scheduled time.
  • Everything is logged in the journal with exit status, so systemctl status backup-home.service tells you whether last night's run failed.
  • RandomizedDelaySec spreads jobs out, so a fleet of machines doesn't hit the backup server at the same second.
  • Resource controls (Nice=, IOSchedulingClass=, MemoryMax=, CPUQuota=) and sandboxing options apply to the job.
  • No overlap: if the previous run is still going, the timer doesn't start a second copy of the service.

OnCalendar syntax

The format is DayOfWeek Year-Month-Day Hour:Minute:Second, with * as a wildcard and shortcuts such as hourly, daily, weekly and monthly. Check any expression before using it:

systemd-analyze calendar "Mon..Fri *-*-* 09:00"
systemd-analyze calendar "*:0/15"          # every 15 minutes
systemd-analyze calendar "Sun *-*-1..7 03:00"  # first Sunday of the month

For "every N minutes after the last run" rather than wall-clock times, use OnBootSec= and OnUnitActiveSec= instead of OnCalendar=.

User timers (no root needed)

Put the same two files in ~/.config/systemd/user/, drop the User= line, and manage them with systemctl --user. User timers only run while you are logged in, unless you enable lingering:

systemctl --user daemon-reload
systemctl --user enable --now backup-home.timer
sudo loginctl enable-linger "$USER"   # keep user timers running after logout

One-off and quick timers

systemd-run creates a transient timer without writing any files, which is handy for "run this once in an hour":

systemd-run --user --on-active=1h /home/alice/bin/remind.sh
systemd-run --on-calendar="*-*-* 23:00" /usr/local/bin/cleanup.sh   # as root

Which one to use

cronsystemd timer
SetupOne lineTwo unit files
LogsOnly what you redirectJournal, with exit status
Machine was offRun is skipped (without anacron)Persistent=true catches up
Overlapping runsPossiblePrevented
Resource limitsManual (nice, ionice)Built in
PortabilityAny Unix-like systemsystemd-based Linux only

Related reading