systemd Timers vs Cron: Scheduling Jobs Properly - 夜莺博客

systemd Timers vs Cron: Scheduling Jobs Properly

cron still works, and it still has the same three problems it had twenty years ago: no logging unless you redirect it, no idea what happened if the machine was off, and no dependency handling. systemd timers solve all three with a unit pair you probably already know how to manage. This guide covers the two timer types, the calendar syntax worth memorising, the catch-up behaviour that makes timers genuinely better for maintenance jobs, and how to migrate without breaking anything.

The structure: a timer plus a service

Unlike cron's single line, a timer is a .timer unit that activates a matching .service unit. The service does the work; the timer decides when.

# /etc/systemd/system/backup.timer
[Unit]
Description=Run nightly backup

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

[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup job
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh
Nice=10
IOSchedulingClass=idle
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers
systemctl list-timers --all

Realtime vs monotonic timers

Type Directive Behaviour
Realtime (wallclock) OnCalendar= Fires at a calendar time, like cron. Gets an implicit dependency on time-sync.target so it will not run before the clock is set.
Monotonic OnBootSec=, OnStartupSec=, OnUnitActiveSec=, OnUnitInactiveSec=, OnActiveSec= Fires relative to an event (boot, unit activation, previous run). Stops counting while the machine is suspended or off.

The two can be combined in one timer, which is handy for "run an hour after boot, and then daily":

[Timer]
OnBootSec=1h
OnUnitActiveSec=24h

Calendar syntax worth memorising

OnCalendar=hourly
OnCalendar=daily
OnCalendar=weekly
OnCalendar=Mon..Fri *-*-* 08:00:00
OnCalendar=*-*-1,15 03:00:00          # 1st and 15th of every month
OnCalendar=*-*-* 00/6:00:00           # every 6 hours
OnCalendar=*-*-* 02:00:00 Europe/Paris # explicit timezone

# Validate before you commit
systemd-analyze calendar "Mon..Fri *-*-* 08:00:00"
systemd-analyze calendar --iterations=5 "*-*-* 00/6:00:00"

systemd-analyze calendar prints the next elapse times, which is the only reliable way to check a complicated expression. A misconfigured OnCalendar silently produces a timer that never fires — the most common migration bug.

Why Persistent=true matters

[Timer]
OnCalendar=daily
Persistent=true

Persistent=true records the last trigger time in /var/lib/systemd/timers/ and, if the machine was powered off when the timer should have fired, runs the job immediately after boot. For cron, "the backup did not run because the host was down" was a recurring excuse; with persistent timers, catch-up is a one-line setting.

Add jitter when many hosts share a schedule:

[Timer]
RandomizedDelaySec=600       # spread triggers over 10 minutes
FixedRandomDelay=true        # stable per-machine offset
AccuracySec=1min             # default is 1min; raise to batch wakeups

Operations: the part cron never had

# Trigger the job now, regardless of the timer
sudo systemctl start backup.service

# Full history, output and exit status
systemctl status backup.service
journalctl -u backup.service --since today
journalctl -u backup.timer --since "2 days ago"

# Diagnose a timer that does not fire
systemctl list-timers --all | grep backup
systemd-analyze verify /etc/systemd/system/backup.timer
ls -l /var/lib/systemd/timers/

Logging is the killer feature. journalctl -u backup.service shows stdout/stderr, exit code, and duration without a single redirect in the job itself.

Migration checklist

  1. Translate the schedule with systemd-analyze calendar and verify the next five elapse times.
  2. Move the job logic into a script; keep the service unit to ExecStart, user, environment and resource limits.
  3. Add Persistent=true for periodic maintenance jobs; skip it for jobs that must not run twice (for example an idempotent-by-design reporter is fine, a non-idempotent data load is not).
  4. Set RandomizedDelaySec if the job hits a shared resource, and Nice/IOSchedulingClass if it is heavy.
  5. Disable the old crontab entry in the same change, not "later" — duplicate execution is the classic migration incident.
  6. After a week, compare systemctl list-timers output with expectations; if a timer shows a next elapse far in the future, check the calendar expression and the unit's Requires chain.

Timers are not just cron with more syntax: the logging, the catch-up behaviour and the dependency handling are exactly the three things you need when a scheduled job matters.

Related Reading on This Site

原文链接:https://wiki.archlinux.org/title/Systemd/Timers (ArchWiki - systemd/Timers)