Назад в блог

Cron vs systemd timers: which should you use?

11 сентября 2026 г.

On a modern distro, use a systemd timer for anything you own and keep cron for one-line user jobs and servers you did not set up yourself. Timers log every run to the journal, can catch up on schedules missed while the machine was off, and depend on the same unit files as everything else on the system. Cron wins on brevity — one crontab -e line beats two unit files — and it is still the only scheduler guaranteed to exist on minimal containers and exotic Unix boxes. The catch: cron fails silently. If your job errors at 3 a.m., cron mails a local mailbox nobody reads, while a timer gives you journalctl -u mytimer.service with the full output. Below: the real differences, how to test each one, why a timer might not trigger, and a decision table.

What is the difference between cron and a systemd timer?

Cron is a daemon that reads a table of lines — five time fields and a command — and runs each command when the wall clock matches. That is the whole model. It has no concept of a job as an object: no unit, no status, no dependencies, no log entry of its own.

A systemd timer is a unit file (foo.timer) that fires another unit file (foo.service) when its schedule matches. The job is a first-class object with start, status, logs, and failure tracking like any other service. Scheduling is either calendar-based (cron-like) or monotonic (OnBootSec=15min, which wall-clock changes cannot confuse).

The practical consequences:

  • Logging: timers log stdout/stderr to the journal per unit; cron at best mails the local user.
  • Missed runs: a timer with Persistent=true runs once on boot if its schedule was missed; cron just skips.
  • Dependencies: timers can wait for network-online.target or mount points; cron needs you to hand-roll retry logic in the script.
  • Syntax: cron is one line; a timer is two files. That is the entire cost of switching.

Which schedule syntax is easier — crontab or OnCalendar?

Cron’s five fields are the compact incumbent: */15 * * * * is every 15 minutes, and most admins can read them in their sleep. Systemd’s OnCalendar= is more verbose but strictly more expressive, and systemd-analyze calendar will tell you the next runs before you commit — cron has no equivalent dry-run.

# Cron: every day at 03:30
30 3 * * * /usr/local/bin/backup.sh

# systemd: the same schedule, verifiable before you save it
systemd-analyze calendar "*-*-* 03:30:00"
# -> Next elapse: Fri 2026-09-11 03:30:00 ...

OnCalendar handles things cron genuinely cannot express cleanly: Mon..Fri *-*-* 09..17:00:00 (weekdays, business hours), or *:0/15 with RandomizedDelaySec=10m to stop a thousand machines from hammering a server at the same instant.

How do I test a systemd timer without waiting?

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.

How do I run a cron job manually?

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 same 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. Most silent cron failures are a bare PATH or an unquoted % in the command (cron treats % as a newline), and both show up immediately this way.

Why is my systemd timer not triggering?

Four causes cover almost every case, in the order to check them:

  1. Service and timer names do not match. foo.timer fires foo.service — a typo’d OnFailure or a renamed unit means the timer “runs” into nothing. systemctl cat foo.timer shows exactly what it targets.
  2. The timer was edited but not reloaded. After changing a unit file: sudo systemctl daemon-reload && sudo systemctl restart foo.timer. Without this your new schedule is not live.
  3. Persistent=true without OnCalendar semantics you expect — or you are checking systemctl status foo.timer (always shows active even when idle) instead of list-timers.
  4. The service it fires is failing instantly, so the timer looks dead. journalctl -u foo.service --since -1h will show the crash that list-timers hides.

And when you do look at the logs, the journalctl cheat sheet covers the filters (-u, --since, -f) that make this fast.

Cron vs systemd timers: the decision table

cronsystemd timer
Setupone crontab linetwo unit files
Logginglocal mail, usually unreadjournal, per unit
Missed run (machine off)skippedruns once with Persistent=true
Dependencies / orderingnone — DIY in scriptfull unit dependencies
Randomised delayDIY with $RANDOMRandomizedDelaySec=
Test/dry-run schedulenosystemd-analyze calendar
Exists everywhereyes — containers, BSDs, embeddedneeds systemd (PID 1)
User jobs without rootcrontab -esystemd --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. A common pattern is a timer running a maintenance job — say a nightly rsync backup — where Persistent=true guarantees the backup happens even if the machine was asleep at the scheduled minute, which cron simply cannot offer.

— mrsaynothing

llama.cpp vs Ollama: Which Should You Run in 2026?