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.
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/2 … 0/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 fields — Fri *-*-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=truestores 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.serviceshows every run’s full output with timestamps;systemctl list-timerslists 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=300jitters 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 --usertimers 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. @rebootand trivial schedules.OnBootSecis fine, but@reboot backupis 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.