Server

systemd Timers vs Cron: When to Use Which

By Prabath Thalangama· October 24, 2026· 3 min read
#linux#systemd#automation

Introduction

Cron still works fine for simple recurring jobs. systemd timers add things cron never had: journal logging per run, catch-up for missed runs, randomized start times to avoid thundering herds, and the full [Service] sandbox. Here's the comparison and a migration path.

What timers add over cron

Need cron systemd timer
See a job's output/exit status Mail, if configured journalctl -u job.service always
Run a missed job after downtime No (anacron partly) Persistent=true
Spread load (100 hosts, not all at :00) No RandomizedDelaySec=
Resource limits, sandboxing No MemoryMax=, Nice=, ProtectSystem=, etc.
Dependencies ("after the DB is up") No After=, Requires=
Run only on AC power / when idle No ConditionACPower=, etc.

Cron is still fine for a trivial job on one machine where you don't need any of that, and it's one file instead of two units.

Hands-on

A timer is two units: foo.service (what to run) and foo.timer (when).

/etc/systemd/system/backup.service:

[Unit]
Description=Nightly backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Nice=10
IOSchedulingClass=idle

/etc/systemd/system/backup.timer:

[Unit]
Description=Run backup nightly at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=15m

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

Enable the timer, not the service.

OnCalendar syntax

OnCalendar=hourly                       # top of every hour
OnCalendar=*-*-* 02:30:00               # daily 02:30
OnCalendar=Mon,Thu *-*-* 06:00:00       # Mon and Thu 06:00
OnCalendar=*-*-1 04:00:00               # first of every month
OnCalendar=*:0/15                       # every 15 minutes
OnCalendar=Sat *-*-1..7 12:00:00        # first Saturday of the month

Test any expression:

systemd-analyze calendar "Mon *-*-* 06:00:00" --iterations 3

Monotonic timers (relative to boot / activation)

[Timer]
OnBootSec=15min                 # 15 min after boot
OnUnitActiveSec=1h              # then every hour after each run

Verification and troubleshooting

systemctl list-timers --all              # next run, last run, unit
systemctl status backup.timer
journalctl -u backup.service --since today
systemd-analyze verify backup.timer      # syntax check
  • Timer enabled but never fires — you enabled backup.service instead of backup.timer, or the .timer has no [Install] section (WantedBy=timers.target).
  • Persistent=true didn't catch up a missed run — Persistent needs a driftfile-style stamp in /var/lib/systemd/timers/; it works only for OnCalendar (not monotonic timers), and the machine must have been off, not the timer disabled.
  • Job runs but does nothing — the service runs in a clean environment: no $PATH beyond a minimal default, no login shell, CWD is /. Use absolute paths, set Environment= or EnvironmentFile=, WorkingDirectory=.
  • Runs as root when it shouldn't — add User= to [Service].
  • Overlapping runs (a slow job started again before finishing) — systemd won't start a oneshot service that's already running for that unit, so timers naturally serialize. Cron does not.
  • OnCalendar fires at the wrong time — timezone. Timers use the system timezone unless you add OnCalendar=... UTC. Check timedatectl.

Converting a crontab

# crontab:   30 2 * * *   /usr/local/bin/backup.sh
# becomes:   OnCalendar=*-*-* 02:30:00

# crontab:   */10 * * * *  /usr/local/bin/poll.sh
# becomes:   OnCalendar=*:0/10

systemd-cron (a package) auto-generates timer units from /etc/cron.* if you want a bulk migration without rewriting each one.

PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.