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.