Why not cron
cron does one thing: "run a script at this minute." A systemd timer can do more:
- Run after an interval from an event (5 minutes after boot, 1 hour after the previous run), beyond a fixed point in time.
- Catch up on missed runs after the host was powered off (
Persistent=true). - Write stdout/stderr automatically to cmd-journalctl, with no
>> /var/log/myapp.log 2>&1in the script. - Use the whole systemd infrastructure: dependencies (
After=/Requires=), sandbox isolation (PrivateTmp=,ProtectSystem=), cgroup limits (MemoryMax=). - Randomize the start (
RandomizedDelaySec=) so that 200 hosts do not hit the backup server at the same moment.
cron is still here, and it is not deprecated. For new code a systemd timer is more convenient.
Pairing .timer + .service
A timer always works alongside a service unit with the same name (or one named explicitly):
/etc/systemd/system/backup.timer ← when to run
/etc/systemd/system/backup.service ← what to run
By default backup.timer triggers backup.service. If the names
differ, name the service explicitly:
[Timer]
Unit=other-backup.service
Trigger types
| Directive | Type | When it fires |
|---|---|---|
OnCalendar= | calendar | At the given calendar time (like cron) |
OnBootSec= | monotonic | N after the kernel boots |
OnStartupSec= | monotonic | N after systemd starts (≈ the same as boot) |
OnActiveSec= | monotonic | N after the timer itself is activated |
OnUnitActiveSec= | monotonic | N after the paired service last started |
OnUnitInactiveSec= | monotonic | N after the paired service last finished |
Monotonic = counted from an event, not from the wall clock. It is not persistent by default (the counter resets after a reboot). This is handy for "run 10 minutes after boot, then every 6 hours."
You can combine these. Put several On* in one [Timer], and it fires on any
condition.
OnCalendar syntax
Format: DOW YYYY-MM-DD HH:MM:SS. DOW and any fields can be omitted (*).
| Expression | What it means |
|---|---|
*-*-* 03:00:00 | every day at 03:00 |
Mon..Fri *-*-* 09:00 | on weekdays at 09:00 |
Mon *-*-* 00:00:00 | every Monday at midnight (= weekly) |
*-*-01 00:00:00 | the first of every month (= monthly) |
*-*-* *:0/15:00 | every 15 minutes |
2026-6,7,8-1,15 01:15:00 | June 1 and 15, July, August 2026 |
Mon *-05~03 | first Monday counting third from the end of May |
*-05~03/2 | third from the end of May, then every 2 days |
Aliases: minutely, hourly, daily, weekly, monthly, yearly.
Always check an expression before you commit it:
systemd-analyze calendar "Mon..Fri *-*-* 09:00"
# Original form: Mon..Fri *-*-* 09:00
# Normalized form: Mon..Fri *-*-* 09:00:00
# Next elapse: Wed 2026-04-30 09:00:00 EDT
# From now: 14h left
AccuracySec and RandomizedDelaySec, why it is not exact
By default systemd does not fire a timer at the exact second you specify. It fires in a
1-minute window starting from the given time. This is deliberate: if 5 timers are
set to daily (= 00:00:00), they would start at once and saturate I/O.
[Timer]
OnCalendar=daily
AccuracySec=1us # narrow window, fires at the exact second
RandomizedDelaySec=30min # plus a random delay of up to 30 min
Most system timers default to AccuracySec=1h or more.
Narrow it only when the job really is time-critical.
Persistent, catching up after downtime
By default, if the host was powered off at the firing time, the event is
missed. With Persistent=true (for OnCalendar= only) systemd
records the last run time in /var/lib/systemd/timers/ and checks it at
startup. If a run was missed, it triggers right away.
[Timer]
OnCalendar=daily
Persistent=true # critical for backups on laptops and dev VMs
Minimal example: backup every 6 hours
/etc/systemd/system/backup.service:
[Unit]
Description=Nightly backup
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/do-backup.sh
User=backup
/etc/systemd/system/backup.timer:
[Unit]
Description=Backup every 6h after boot
[Timer]
OnBootSec=15min
OnUnitActiveSec=6h
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
Activate it:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
Note: WantedBy=timers.target (not multi-user.target),
otherwise the timer will not be picked up at boot.
Viewing and debugging
systemctl list-timers --all # all timers with next/last run
systemctl status backup.timer # state of one timer
systemctl cat backup.timer # the effective unit with drop-ins
journalctl -u backup.service -S today # what the triggered service wrote
systemd-analyze calendar 'Mon *-*-* 04:00' # check OnCalendar
systemd-analyze timespan '15days 6h' # parse a time span
Remove an unwanted default timer (no SSD means no need for fstrim):
sudo systemctl disable --now fstrim.timer