systemd Cheatsheet
Timers
Use this systemd reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
A Timer Plus Its Service
A timer never contains the work. It triggers a .service of the same base name, so you always write two files.
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh# /etc/systemd/system/backup.timer [Unit] Description=Run the nightly backup [Timer] OnCalendar=*-*-* 02:30:00 Persistent=true RandomizedDelaySec=15m AccuracySec=1m [Install] WantedBy=timers.target
sudo systemctl daemon-reload sudo systemctl enable --now backup.timer # enable the TIMER, not the service systemctl list-timers backup.timer systemctl start backup.service # run it once, right now
Enable the .timer, never the .service. The service needs no [Install] section at all. Point at a differently named service with Unit= in the [Timer] section.
The service must be Type=oneshot without RemainAfterExit=yes. A unit that stays active is never re-triggered, because the timer starts it and finds it already running.
OnCalendar= Syntax
The shape is DayOfWeek Year-Month-Day Hour:Minute:Second. Any field may be *, a comma list, a .. range, or a /-step. Omit the date and *-*-* is assumed. Omit the time and 00:00:00 is assumed. Omit seconds and :00 is assumed.
| Expression | Fires |
|---|---|
*-*-* 02:30:00 | Every day at 02:30 |
02:30 | Same thing, shorter |
Mon..Fri 09:00 | Weekdays at 09:00 |
Mon,Thu 18:00 | Mondays and Thursdays at 18:00 |
*-*-* *:00:00 | Top of every hour |
*-*-* *:0/15 | Every 15 minutes |
*-*-* 0/6:00:00 | Every 6 hours, from midnight |
*-*-01 00:00:00 | First of every month |
*-*-01,15 03:00 | 1st and 15th at 03:00 |
*-01,04,07,10-01 | Quarterly, on the 1st |
Mon *-*-* 00:00:00 | Every Monday at midnight |
*-02~03 | Third-to-last day of February |
Mon *-05~07/1 | Last Monday in May |
Sat *-*-1..7 12:00 | First Saturday of each month at noon |
2027-01-01 00:00:00 | Once, absolutely |
*-*-* 03:00:00 UTC | Daily at 03:00 UTC, not local time |
*-*-* 03:00 Europe/Madrid | Daily at 03:00 in that IANA zone |
Shorthands, all normalized by systemd:
| Shorthand | Normalized form |
|---|---|
minutely | *-*-* *:*:00 |
hourly | *-*-* *:00:00 |
daily | *-*-* 00:00:00 |
weekly | Mon *-*-* 00:00:00 |
monthly | *-*-01 00:00:00 |
quarterly | *-01,04,07,10-01 00:00:00 |
semiannually | *-01,07-01 00:00:00 |
yearly / annually | *-01-01 00:00:00 |
OnCalendar= may appear more than once, and the timer fires whenever any expression elapses. A bare empty OnCalendar= resets the whole list, which is how you replace a packaged schedule from a drop-in.
Verify Before You Trust It
Never guess a calendar expression. Ask systemd.
systemd-analyze calendar "Mon..Fri 09:00" systemd-analyze calendar --iterations=5 "*-*-* 0/6:00:00" systemd-analyze calendar "Sat *-*-1..7 12:00"
$ systemd-analyze calendar "Mon..Fri 09:00" Original form: Mon..Fri 09:00 Normalized form: Mon..Fri *-*-* 09:00:00 Next elapse: Tue 2026-09-15 09:00:00 CEST (in UTC): Tue 2026-09-15 07:00:00 UTC From now: 21h left
A typo gets you Failed to parse calendar expression and a non-zero exit, so this works in CI too. systemd-analyze timespan 90min does the same normalization for durations.
Monotonic Timers
Relative to an event rather than a wall clock. Use these for "some time after boot" work.
| Directive | Relative to |
|---|---|
OnActiveSec= | The timer unit itself being activated |
OnBootSec= | Machine boot |
OnStartupSec= | The service manager starting (for --user, your login) |
OnUnitActiveSec= | The last time the triggered unit was activated |
OnUnitInactiveSec= | The last time the triggered unit went inactive |
[Timer] OnBootSec=5min OnUnitActiveSec=1h
That is the "every hour, first run 5 minutes after boot" idiom. OnUnitActiveSec= measures from the start of the last run, OnUnitInactiveSec= from its end, which is what you want when the job's duration varies. Combine monotonic and calendar expressions freely in one timer.
Accuracy, Jitter, and Catch-Up
| Directive | Default | Effect |
|---|---|---|
Persistent= | false | Store the last trigger time on disk and run immediately at boot if a run was missed. OnCalendar= only |
RandomizedDelaySec= | 0 | Delay by a random amount in 0..value, to spread load across a fleet |
FixedRandomDelay= | false | Make that random offset stable per machine instead of re-rolled each time |
AccuracySec= | 1min | Window systemd may coalesce timers within, to reduce wakeups |
WakeSystem= | false | Resume a suspended machine to run the job |
RemainAfterElapse= | true | Keep an elapsed one-shot timer loaded and queryable |
OnClockChange=, OnTimezoneChange= | false | Also trigger on clock jumps or timezone edits (v242+) |
AccuracySec=1us makes a timer fire nearly exactly on time at the cost of a dedicated wakeup. Do not set it below a second without a reason. RandomizedDelaySec= applies on top of Persistent= catch-up runs too.
State for Persistent= lives in /var/lib/systemd/timers/stamp-<unit>. Delete that file to make the next boot look like a first run.
Inspecting and Ad-Hoc Timers
systemctl list-timers # active timers, sorted by next elapse systemctl list-timers --all # include inactive ones systemctl status backup.timer systemctl show -p NextElapseUSecRealtime -p LastTriggerUSec backup.timer journalctl -u backup.service -b # the job's own output systemctl clean --what=state backup.service
NEXT LEFT LAST PASSED UNIT Tue 2026-09-15 02:30:00 CEST 12h left Mon 2026-09-14 02:30:12 CEST 11h ago backup.timer
Transient timers need no files at all:
systemd-run --on-calendar="*-*-* 04:00" /usr/local/bin/report systemd-run --on-active=90s --unit=ping-once /usr/bin/curl -sf https://example.com systemd-run --on-boot=2min --timer-property=AccuracySec=10s /usr/local/bin/warmup
Timers vs cron
| cron | systemd timer | |
|---|---|---|
| Schedule syntax | */15 * * * * | OnCalendar=*-*-* *:0/15 |
| Missed runs | Lost | Persistent=true catches up |
| Logs | Mailed, or wherever you redirect | journalctl -u foo.service |
| Environment | Minimal shell env, surprises everyone | Explicit Environment=, EnvironmentFile= |
| Overlap | Two copies can run at once | The unit is already active, so no second run |
| Resource limits | None | MemoryMax=, CPUQuota=, TasksMax= |
| Sandboxing | None | Full Protect* and SystemCall* set |
| Dependencies | None | After=, Requires=, ConditionACPower= |
| Randomized jitter | Hand-rolled sleep $RANDOM | RandomizedDelaySec= |
| Per-user | crontab -e | systemctl --user, needs linger |
| Dry run | None | systemd-analyze calendar |
| Files needed | One line | Two files |
cron wins on brevity. Timers win on everything you care about at 3am, which is why distributions now ship logrotate.timer, fstrim.timer and apt-daily.timer instead of crontab entries.