Skip to main content

How systemd Timers Work

Learning Focus

By the end of this lesson you will understand how systemd separates scheduling from execution using the two-unit model, know the difference between calendar and monotonic timers, and be able to trace the complete lifecycle from unit creation to log reading.

The Two-Unit Model​

Every systemd timer requires two unit files working together:

  1. Timer unit (name.timer) — defines when to run.
  2. Service unit (name.service) — defines what to run.
Core Rule

A .timer unit never executes a command directly. It only activates a .service unit at the scheduled time. The service unit runs the actual script or command.

Automatic Name Matching​

If the Unit= directive is omitted in the .timer file, systemd automatically looks for a .service with the same base name:

wp-backup.timer ──► wp-backup.service ──► /usr/local/bin/wp-backup.sh
health-check.timer ──► health-check.service ──► /usr/local/bin/health-check.sh
certbot-renew.timer ──► certbot-renew.service ──► /usr/bin/certbot renew

You can override this with Unit=another-service.service in the [Timer] section, but the default name-matching convention is what most setups use.

State Separation​

The timer unit and service unit have separate states. Understanding this is critical for debugging:

UnitWhat It Tells You
wp-backup.timerNext run time, last trigger time, schedule expression, enabled state
wp-backup.serviceExit code, stdout/stderr output, runtime duration, failure reason
check-both-states.sh
# Always check BOTH units when debugging
systemctl status wp-backup.timer --no-pager
systemctl status wp-backup.service --no-pager
example-timer-status.txt
● wp-backup.timer - Run WordPress backup nightly at 02:15
Loaded: loaded (/etc/systemd/system/wp-backup.timer; enabled)
Active: active (waiting) since Sun 2026-03-01 02:15:03 UTC; 21h ago
Trigger: Mon 2026-03-02 02:15:00 UTC; 2h 14min left
Triggers: ● wp-backup.service
example-service-status.txt
● wp-backup.service - WordPress nightly backup
Loaded: loaded (/etc/systemd/system/wp-backup.service; static)
Active: inactive (dead) since Sun 2026-03-01 02:16:10 UTC; 21h ago
Process: 1201 ExecStart=/usr/local/bin/wp-backup.sh (code=exited, status=0/SUCCESS)
Main PID: 1201 (code=exited, status=0/SUCCESS)

Timer Types: Calendar vs Monotonic​

systemd supports two fundamentally different scheduling models:

Calendar Timers (Wall-Clock)​

Calendar timers fire at specific wall-clock times — like cron. They use the OnCalendar= directive.

[Timer]
OnCalendar=02:15 # Every day at 2:15 AM
OnCalendar=Mon..Fri 06:00 # Weekdays at 6 AM
OnCalendar=monthly # 1st of each month at midnight

Use when: You need a job to run at a specific time of day or on a specific date — backups, reports, maintenance windows.

Monotonic Timers (Relative/Interval)​

Monotonic timers count relative to lifecycle events — not the wall clock. They use directives like OnBootSec=, OnUnitActiveSec=, and OnUnitInactiveSec=.

[Timer]
OnBootSec=2min # 2 minutes after system boot
OnUnitActiveSec=1h # 1 hour after last activation
OnUnitInactiveSec=20m # 20 minutes after last run completed

Use when: You need a job to run at regular intervals regardless of the time of day — heartbeats, health checks, retry loops.

Comparison​

AspectCalendar (OnCalendar=)Monotonic (OnBootSec=, etc.)
Based onWall-clock timeElapsed time since event
Supports Persistent=trueYesNo
Timezone awareYes (append timezone)No (relative timing)
Schedule validationYes (systemd-analyze calendar)No
Drifts if system load delays runNo (fixed to clock)Yes (counts from last event)
Best forBackups, reports, maintenanceHeartbeats, health checks, retries

Combining Both Types​

You can combine calendar and monotonic directives in a single timer:

combined-timer.timer
[Timer]
OnBootSec=30s # Run 30 seconds after boot
OnUnitActiveSec=1h # Then repeat every hour

This runs the service 30 seconds after boot, then every hour after each activation.

The Lifecycle​

Step-by-Step Breakdown​

StepWhat HappensYour Action
1. Create filesWrite the .timer and .service unit filesPlace in /etc/systemd/system/
2. Reloadsystemd reads the new files from disksudo systemctl daemon-reload
3. Enablesystemd creates symlinks and starts the timersudo systemctl enable --now name.timer
4. WaitTimer counts down to the next scheduled timeNothing — automatic
5. FireSchedule matches → systemd starts the serviceNothing — automatic
6. ExecuteThe ExecStart command runsYour script does its work
7. Logstdout/stderr are captured by journaldjournalctl -u name.service
8. RepeatTimer resets and waits for the next scheduleAutomatic

Where Timer Units Live​

System Units (All Users, Root Required)​

LocationPurposeWho Manages It
/etc/systemd/system/Admin-created custom units and overridesroot
/usr/lib/systemd/system/Package-provided units (do not edit directly)Package manager
/etc/systemd/system/name.d/override.confOverride a package unit without replacing itroot

User Units (Per User, No Root Required)​

LocationPurpose
~/.config/systemd/user/Per-user timer and service units

User units run inside the user's session:

user-unit-management.sh
mkdir -p ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now my-timer.timer
systemctl --user list-timers --no-pager
User Unit Persistence

By default, user units stop when the user logs out. To keep them running:

sudo loginctl enable-linger "$USER"

How systemd Timers Compare to Cron​

Side-by-Side​

FeatureCronsystemd timer
Schedule formatm h dom mon dow (5 fields)OnCalendar= expression
ConfigurationOne line per job in crontabTwo files per job (.timer + .service)
LoggingManual (redirect >> file 2>&1)Automatic (journald)
Missed runsSilently skippedPersistent=true catches up
Overlap preventionManual (flock)flock + systemd oneshot queuing
DependenciesNot availableAfter=, Wants=, Requires=
Security sandboxNot availableProtectSystem=, PrivateTmp=, etc.
JitterNot availableRandomizedDelaySec=
Schedule testingNot availablesystemd-analyze calendar
Resource limitsNot availableRuntimeMaxSec=, CPUQuota=
Visibilitycrontab -l (per user)systemctl list-timers (system-wide)

Cron Expression → OnCalendar Conversion​

CronOnCalendarMeaning
* * * * *minutely or *:*Every minute
0 * * * *hourlyEvery hour
0 0 * * *dailyMidnight daily
0 2 * * *02:00Daily at 2 AM
15 2 * * *02:15Daily at 2:15 AM
0 6 * * 1-5Mon..Fri 06:00Weekdays at 6 AM
*/5 * * * **:0/5Every 5 minutes
*/15 * * * **:0/15Every 15 minutes
0 0 1 * *monthly1st of month
0 0 * * 0weekly or Sun *-*-*Weekly on Sunday
0 3 * * 6Sat 03:00Saturday at 3 AM

When to Use Cron vs systemd Timer​

Your First Timer — Hands-On Walkthrough​

Step 1 — Create the Service Unit​

/etc/systemd/system/heartbeat.service
[Unit]
Description=Write a heartbeat timestamp

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo "[$(date +%%F\ %%T)] heartbeat" >> /tmp/heartbeat.log'

Step 2 — Create the Timer Unit​

/etc/systemd/system/heartbeat.timer
[Unit]
Description=Fire heartbeat every 60 seconds

[Timer]
OnUnitActiveSec=60s

[Install]
WantedBy=timers.target

Step 3 — Reload, Enable, and Verify​

activate-heartbeat.sh
sudo systemctl daemon-reload
sudo systemctl enable --now heartbeat.timer

Step 4 — Confirm It Is Scheduled​

check-heartbeat.sh
systemctl list-timers heartbeat.timer --no-pager
expected-output.txt
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-03-02 03:06:00 UTC 58s left n/a n/a heartbeat.timer heartbeat.service

Step 5 — Test Immediately​

test-now.sh
sudo systemctl start heartbeat.service
cat /tmp/heartbeat.log
expected-output.txt
[2026-03-02 03:05:02] heartbeat

Step 6 — Read the Journal​

read-journal.sh
journalctl -u heartbeat.service -n 5 --no-pager

You now know the complete lifecycle: create → reload → enable → verify → test → read logs.

Key Takeaways​

  • A .timer unit always works with a .service unit — it never runs commands directly.
  • Calendar timers (OnCalendar=) fire at wall-clock times — like cron but with more power.
  • Monotonic timers (OnBootSec=, OnUnitActiveSec=) fire after elapsed time — for heartbeats and retries.
  • Always check both the .timer status (next/last run) and the .service logs when debugging.
  • systemd timers replace cron with native logging, missed-run catch-up, and security sandboxing.

What's Next​

  • OnCalendar Syntax — master the calendar expression format for wall-clock scheduling.