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.

ExpressionFires
*-*-* 02:30:00Every day at 02:30
02:30Same thing, shorter
Mon..Fri 09:00Weekdays at 09:00
Mon,Thu 18:00Mondays and Thursdays at 18:00
*-*-* *:00:00Top of every hour
*-*-* *:0/15Every 15 minutes
*-*-* 0/6:00:00Every 6 hours, from midnight
*-*-01 00:00:00First of every month
*-*-01,15 03:001st and 15th at 03:00
*-01,04,07,10-01Quarterly, on the 1st
Mon *-*-* 00:00:00Every Monday at midnight
*-02~03Third-to-last day of February
Mon *-05~07/1Last Monday in May
Sat *-*-1..7 12:00First Saturday of each month at noon
2027-01-01 00:00:00Once, absolutely
*-*-* 03:00:00 UTCDaily at 03:00 UTC, not local time
*-*-* 03:00 Europe/MadridDaily at 03:00 in that IANA zone

Shorthands, all normalized by systemd:

ShorthandNormalized form
minutely*-*-* *:*:00
hourly*-*-* *:00:00
daily*-*-* 00:00:00
weeklyMon *-*-* 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.

DirectiveRelative 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

DirectiveDefaultEffect
Persistent=falseStore the last trigger time on disk and run immediately at boot if a run was missed. OnCalendar= only
RandomizedDelaySec=0Delay by a random amount in 0..value, to spread load across a fleet
FixedRandomDelay=falseMake that random offset stable per machine instead of re-rolled each time
AccuracySec=1minWindow systemd may coalesce timers within, to reduce wakeups
WakeSystem=falseResume a suspended machine to run the job
RemainAfterElapse=trueKeep an elapsed one-shot timer loaded and queryable
OnClockChange=, OnTimezoneChange=falseAlso 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

cronsystemd timer
Schedule syntax*/15 * * * *OnCalendar=*-*-* *:0/15
Missed runsLostPersistent=true catches up
LogsMailed, or wherever you redirectjournalctl -u foo.service
EnvironmentMinimal shell env, surprises everyoneExplicit Environment=, EnvironmentFile=
OverlapTwo copies can run at onceThe unit is already active, so no second run
Resource limitsNoneMemoryMax=, CPUQuota=, TasksMax=
SandboxingNoneFull Protect* and SystemCall* set
DependenciesNoneAfter=, Requires=, ConditionACPower=
Randomized jitterHand-rolled sleep $RANDOMRandomizedDelaySec=
Per-usercrontab -esystemctl --user, needs linger
Dry runNonesystemd-analyze calendar
Files neededOne lineTwo 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.