systemd Timers vs cron — When to Use Which (with a Translation Table)

systemd timers vs cron — OnCalendar syntax vs crontab fields, catch-up runs with Persistent=true, dependencies, logging, and when each is the right tool.

Published 2026-09-25

Every modern Linux box has two schedulers and they don’t quite overlap. cron is one line of text and half a century of habit; systemd timers are two unit files, real dependencies, and a journal that remembers what ran. Neither replaces the other — here’s the honest split.

The shape of each

A cron entry is the schedule and the job in one line:

30 3 * * *  /opt/jobs/nightly.sh >>/var/log/nightly.log 2>&1

A systemd timer is two files. nightly.timer:

[Unit]
Description=Nightly job

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

[Install]
WantedBy=timers.target

nightly.service:

[Unit]
Description=Nightly job
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/opt/jobs/nightly.sh
StandardOutput=append:/var/log/nightly.log
StandardError=append:/var/log/nightly.log

Then systemctl enable --now nightly.timer. One line became ~20 — and in exchange the job declares what it needs, keeps its own log, and catches up after downtime.

Syntax: crontab fields → OnCalendar

OnCalendar is DayOfWeek Year-Month-Day Hour:Minute:Second, with * wildcards, , lists, .. ranges, and /n repetition. The common translations:

crontab OnCalendar
* * * * * *-*-* *:*:00 (minutely is minutely)
*/15 * * * * *:0/15
30 3 * * * *-*-* 03:30:00 — or just daily + OnCalendar=03:30
0 9 * * 1-5 Mon..Fri *-*-* 09:00:00
0 0 * * 0 Sun *-*-* 00:00:00 — or weekly
0 0 1 * * *-*-01 00:00:00 — or monthly
0 0 29 2 * *-02-29 00:00:00 — leap days just work
*/2 * * * * *:0/20/2:00 for odd hours
@reboot OnBootSec=5min (fires 5 min after boot)

Things OnCalendar says natively that cron literally can’t: Fri *-*-08..14 (second Friday), *-*-* 02:30:00/90 (seconds-level precision), Sat,Sun *-06,07-* (weekends in June–July). Cron’s counter-moves are ?-less simplicity and the fact that the line you paste works on a 1993 HP-UX box too.

And the dom/dow OR rule? OnCalendar always ANDs its fieldsFri *-*-13 is Friday the 13th, exactly what it looks like. The OR trap is a cron-ism; timers sidestep it entirely.

Where timers genuinely win

  • Catch-up runs. Persistent=true stores the last-fire timestamp; after a reboot it immediately runs whatever was missed. Cron has no memory — missed is missed. (anacron fills this gap on cron systems, another daemon to learn.)
  • Dependencies. After=postgresql.service, RequiresMountsFor=/data, ConditionPathExists= — the job waits for what it needs instead of racing boot order.
  • The journal. journalctl -u nightly.service shows every run’s full output with timestamps; systemctl list-timers lists every timer’s last and next fire time — the answer to “when does this actually run?” in one command.
  • Resource control. MemoryMax=, CPUQuota=, Nice= on the service — a runaway job can’t eat the box.
  • Randomized delay. RandomizedDelaySec=300 jitters the fire time — cron’s classic “every server’s 03:00 job hits the NFS at once” has a first-class fix.
  • User services. systemctl --user timers need no root and can run for logged-out users with linger enabled.

Where cron still wins

  • One line. Paste-ready, diff-friendly, grep-able across /etc/crontab + crontab -l. A timer is a directory tree.
  • Ubiquity. Every Unix has cron — containers, BSD, macOS, busybox, that 2014 appliance. systemd is a Linux-systemd fact only.
  • Shared hosting / no systemd user session. If you can’t systemctl, you can’t timer.
  • @reboot and trivial schedules. OnBootSec is fine, but @reboot backup is shorter and self-documenting.

The honest differences summary

cron systemd timer
definition 1 line 2 unit files
missed runs gone Persistent=true catches up
day-field logic dom OR dow AND only
precision 1 minute 1 second
output local mail / self-redirect journal
dependencies none full unit graph
portability everywhere systemd Linux only
jitter manual sleep $RANDOM RandomizedDelaySec=

Rule of thumb: reach for cron when the job is simple, the host is generic, and “one line in a known place” is worth more than plumbing. Reach for a timer when the job needs dependencies, must catch up after downtime, logs matter, or it’s on a systemd box anyway and deserves systemctl list-timers visibility. They coexist happily — plenty of boxes run both.

Frequently asked questions

Can a systemd timer catch up on a run the machine missed while off?

Yes — that's what Persistent=true is for: on boot, the timer immediately triggers any run it would have fired while the machine was down. Classic cron has no equivalent — missed firings are simply gone (that's the gap anacron was invented to fill).

Is OnCalendar the same syntax as cron?

No — it's a different grammar. *-*-* 03:00:00 (year-month-day hour:min:sec) with Mon..Sun, *:0/15-style repetition and shortcuts like daily, weekly, hourly. It can express things cron can't (seconds precision, 'every 2nd Friday', Nth weekday) but it's wordier. The translation table below maps the common cases.

Do timers need two files?

Yes: a .timer unit describing when (OnCalendar=, Persistent=) and a .service unit describing what (ExecStart=). That indirection buys you dependencies, resource limits and journal logging that cron can't express — at the cost of ~20 lines instead of 1.

Where do timer logs go?

The journal — journalctl -u myjob.service --since today shows every run's stdout/stderr, and systemctl list-timers shows last/next fire times for every timer on the box. No MAILTO, no lost output.

Can user accounts run timers without root?

Yes — systemctl --user timers live in ~/.config/systemd/user/, no root needed, and can even outlive logout if loginctl enable-linger is set. That's friendlier than /etc/cron.d for personal jobs.