Own systemd box → timer. One-line user jobs, containers, boxes you did not build → cron.
Cron fails silently — it mails a local mailbox nobody reads. Timers log every run to the journal: journalctl -u mytimer.service.
Persistent=true re-runs jobs missed while the machine was off; cron silently skips them.
The same job — nightly backup at 03:30 — on both clocks, and what each one leaves behind:
CRON · one line
30 3 * * * /usr/local/bin/backup.sh
runs when the wall clock matches. If it fails, the output mails a local mailbox — usually unread. Machine was off at 03:30? Skipped, silently.
did it even fire? journalctl -u cron -n 20 or grep CRON /var/log/syslog
SYSTEMD TIMER · two unit files
# foo.timer
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
# foo.service — the job itself
a first-class unit: status, dependencies, logs per run in the journal. Machine asleep at the hour? Persistent=true runs it once on boot.
check it: systemd-analyze calendar "*-*-* 03:30:00" — the next elapse, before you save
the schedule verdict: cron's five fields are the compact incumbent; OnCalendar is more verbose but strictly more expressive — Mon..Fri 09..17:00:00 is one line, and RandomizedDelaySec stops a thousand machines hammering one server
these clocks re-stamp from the record's own commands — they cannot read your machine's journal; where your journal actually is: sections 04 and 05.
Persistent=true runs once on boot if its schedule was missed; cron just skips.network-online.target or mount points; cron needs you to hand-roll retry logic in the script.OnBootSec=15min — wall-clock changes cannot confuse it.Three commands answer everything — list what is scheduled and when it next fires, run the job by hand exactly as the timer would, then read its logs:
systemctl list-timers --all # every timer, next + last run
sudo systemctl start backup.service # fire the job now, same unit as the timer
journalctl -u backup.service -f # watch its output live
Note the split: systemctl start backup.timer arms the schedule; the service is the job. If list-timers shows your timer, systemctl status backup.service is green, and the journal shows your output, the whole chain works.
verify: systemctl list-timers --all → your timer, last + next run listed
Cron jobs run with a stripped environment, which is why "works in my shell, fails in cron" is a genre of bug. To reproduce cron faithfully, run the command through sh with the environment cron would use:
crontab -l # confirm the exact line
env -i SHELL=/bin/sh PATH=/usr/bin:/bin sh -c '/usr/local/bin/backup.sh'
grep CRON /var/log/syslog | tail # did cron even fire it? (Debian/Ubuntu)
journalctl -u cron -n 20 # same, on systemd distros
That env -i line is the honest manual test: a bare environment with cron's default PATH. The two cron classics — a bare PATH and an unquoted % (cron treats it as a newline) — both show up immediately in that test.
verify: journalctl -u cron -n 20 → your command's CRON entry
A timer that looks dead is usually its service failing instantly. Four causes, in the order to check them:
foo.timer fires foo.service — a typo or a renamed unit means the timer "runs" into nothing. systemctl cat foo.timer shows exactly what it targets.sudo systemctl daemon-reload && sudo systemctl restart foo.timer. Without this your new schedule is not live.systemctl status foo.timer always shows active even when idle — read list-timers instead.journalctl -u foo.service --since -1h shows the crash that list-timers hides.verify: journalctl -u foo.service --since -1h → the crash list-timers hides
| cron | systemd timer | |
|---|---|---|
| Setup | one crontab line | two unit files |
| Logging | local mail, usually unread | journal, per unit |
| Missed run (machine off) | skipped | runs once with Persistent=true |
| Dependencies / ordering | none — DIY in script | full unit dependencies |
| Randomised delay | DIY with $RANDOM | RandomizedDelaySec= |
| Test/dry-run schedule | no | systemd-analyze calendar |
| Exists everywhere | yes — containers, BSDs, embedded | needs systemd (PID 1) |
| User jobs without root | crontab -e | systemd --user units |
Rules of thumb: shipping a job on your own systemd machine → timer. Quick personal reminder or a box you did not build → cron. Anything with dependencies, retries, or a need to know whether it actually ran → timer, always. The classic pattern — a nightly rsync backup under a timer with Persistent=true — guarantees the backup happens even if the machine was asleep at the scheduled minute, which cron simply cannot offer.
end of the two clocks · nothing fetched · everything local