| |

LFCS 64 🐧 systemd Timers

⏱️ Estimated read: 11 min | |

systemd timers schedule work without cron. The timer unit defines when a job runs, and a paired service unit defines what runs. The two files share a base name: backup.timer triggers backup.service. The exam tests this pairing, the OnCalendar= syntax that replaces cron expressions, the Persistent=true directive that catches missed runs after downtime, and the critical enablement detail: systemctl enable --now targets the .timer, not the .service .

The timer model has two scheduling families. Real-time timers use OnCalendar= with calendar expressions that resemble cron but are more expressive: Mon..Fri *-*-* 10:00:00 runs weekdays at 10 AM, and *-*-* *:00/15:00 runs every 15 minutes . Monotonic timers use OnBootSec=, OnUnitActiveSec=, and OnActiveSec= to schedule relative to boot or the last activation, independent of wall-clock time . A single timer can combine both: run five minutes after boot and also every weekday at 10 AM.

This chapter covers the timer-service pairing, OnCalendar= and monotonic directives, Persistent= for missed runs, the systemctl list-timers inspection command, and the exam traps that turn a scheduled job into one that never fires. It assumes familiarity with custom service units from the preceding chapters.

Key point: A timer is a trigger, not a task. The .timer file must reference a .service file with the same base name (or an explicit Unit= directive), and the exam expects both files to exist. Enabling the service instead of the timer produces a job that never runs on schedule.


Why systemd timers are an exam domain

The timer-service pairing is the first trap. A candidate who writes only a .service file and enables it has created a service that runs once at boot and never again. The LFCS practice guidance is explicit: the standard answer to “run a script daily at 3 AM” is two files—backup.service and backup.timer—followed by systemctl enable --now backup.timer . The .service contains the ExecStart= command; the .timer contains the schedule.

OnCalendar= syntax is tested, not just mentioned. The exam may ask for a specific schedule: weekdays only, every 15 minutes, or a particular day of the month. The syntax is documented in man systemd.time, and systemd-analyze calendar validates expressions before deployment . A malformed OnCalendar= line can be silently ignored if the timer has another scheduling directive, making verification essential.

Persistent=true distinguishes a scheduled job from a reliable one. Without it, a timer that was due to fire while the system was powered off does not run when the system comes back. With it, systemd records the last trigger time and fires the missed run immediately after boot . For backups and maintenance tasks, this is the difference between a job that runs and one that silently skips.

The trade-off. Timers are more verbose than a crontab line, but they provide dependency ordering (After=), resource isolation (User=), journal integration, and the ability to catch missed runs. The exam tests the verbosity because production schedulers need it.


a. The timer-service pair and basic creation

A timer file lives in /etc/systemd/system/ and has the .timer extension. Its [Timer] section contains the scheduling directives and, optionally, a Unit= directive pointing to the service to trigger. If Unit= is omitted, systemd looks for a service with the same base name: backup.timer triggers backup.service .

# /etc/systemd/system/backup.service
[Unit]
Description=Backup job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 3 AM

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

The service uses Type=oneshot because the script runs and exits. The timer’s [Install] section uses WantedBy=timers.target, not multi-user.target. The timers.target is the target that systemd uses to activate all timers at boot .

The enablement command targets the timer: systemctl enable --now backup.timer. Starting backup.service manually runs the job once; starting backup.timer activates the schedule. The systemctl list-timers command shows the next and last trigger times, which is the verification step .


b. OnCalendar= and monotonic scheduling

OnCalendar= accepts a calendar expression. The format is DayOfWeek Year-Month-Day Hour:Minute:Second, with ranges, lists, and step values. Examples from the SUSE documentation and common practice include:

ScheduleExpression
Daily at 3 AM*-*-* 03:00:00
Weekdays at 10 AMMon..Fri *-*-* 10:00:00
Every 15 minutes*-*-* *:00/15:00
First of month*-*-01 00:00:00
Mondays at 4 AMMon *-*-* 04:00:00

The .. range and , list syntax are supported: Mon,Tue *-*-01..04 12:00:00 means the first four days of the month at noon, if those days are Monday or Tuesday . The systemd-analyze calendar command validates and explains expressions before deployment .

Monotonic directives schedule relative to events rather than wall-clock time. OnBootSec=5min fires five minutes after boot. OnUnitActiveSec=24h fires 24 hours after the service was last activated . These are useful for jobs that should run at a fixed interval after the previous run, not at a fixed time of day.

A timer can combine real-time and monotonic directives. The SUSE example uses OnBootSec=5min, OnUnitActiveSec=24h, and OnCalendar=Mon..Fri *-*-* 10:00 in the same [Timer] section . Each directive adds a trigger; the service runs when any condition is met.


c. Persistent=, RandomizedDelaySec=, and verification

Persistent=true is the directive that handles missed runs. systemd stores the last trigger time on disk. When the timer activates, if a scheduled run was missed while the system was off, systemd triggers the service immediately . The SUSE documentation states this explicitly: “If the system is off during the expected execution time, the timer is executed once the system is running again” .

RandomizedDelaySec= adds a random delay before the trigger. This prevents multiple timers from firing at the same instant and overwhelming the system. A value like 2min means the service starts at a random time within two minutes of the scheduled time . For jobs that need precise timing, set it to 0 or omit it.

Verification has three commands. systemctl list-timers --all shows all timers with next and last elapse times . systemctl status backup.timer shows the timer’s active state and the service it triggers. journalctl -u backup.service shows the output of the service’s runs, which is where a failed script’s error appears .

A subtle trap: malformed directives in a timer with another valid scheduling directive can be silently ignored. The SUSE documentation describes a timer with OnClendar= (misspelled) and a valid OnBootSec=. The misspelled line is ignored, and the weekday trigger never fires. The only way to detect this is systemd-analyze verify FILE .


Complete Example Session

# ============================================
# PART 1: THE SERVICE (WHAT RUNS)
# ============================================
# File: /etc/systemd/system/backup.service

[Unit]
Description=Daily backup job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backupuser

# ============================================
# PART 2: THE TIMER (WHEN IT RUNS)
# ============================================
# File: /etc/systemd/system/backup.timer

[Unit]
Description=Run backup daily at 3 AM

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=5min

[Install]
WantedBy=timers.target

# ============================================
# PART 3: SECTION BREAKDOWN
# ============================================
# [Timer]
#   OnCalendar=*-*-* 03:00:00 → daily at 3 AM.
#   Persistent=true → catch missed runs after downtime.
#   RandomizedDelaySec=5min → spread load.
#
# [Install]
#   WantedBy=timers.target → enable at boot.
#   NOT multi-user.target. Timers use timers.target.

# ============================================
# PART 4: VERIFY BEFORE ENABLING
# ============================================
# Check syntax.
systemd-analyze verify /etc/systemd/system/backup.timer

# Validate the calendar expression.
systemd-analyze calendar "*-*-* 03:00:00"

# ============================================
# PART 5: RELOAD, ENABLE, VERIFY
# ============================================
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

# Verify the timer is active.
systemctl is-active backup.timer   # active
systemctl list-timers backup.timer # shows next trigger

# ============================================
# PART 6: MONOTONIC SCHEDULING
# ============================================
# Run 5 minutes after boot, then every 24h.
# [Timer]
# OnBootSec=5min
# OnUnitActiveSec=24h
#
# These are independent of wall-clock time.

# ============================================
# PART 7: COMBINED REAL-TIME AND MONOTONIC
# ============================================
# [Timer]
# OnBootSec=5min
# OnUnitActiveSec=24h
# OnCalendar=Mon..Fri *-*-* 10:00
#
# Service runs when ANY condition is met.

# ============================================
# PART 8: THE SILENT IGNORE TRAP
# ============================================
# A misspelled OnClendar= with a valid OnBootSec=
# is silently ignored. The weekday trigger never fires.
# Only systemd-analyze verify catches it.
systemd-analyze verify /etc/systemd/system/backup.timer
# Output: Unknown key name 'OnClendar' in section 'Timer'

# ============================================
# PART 9: INSPECT AND TROUBLESHOOT
# ============================================
# List all timers.
systemctl list-timers --all

# Status of the timer.
systemctl status backup.timer

# Service output (where the script's errors appear).
journalctl -u backup.service -n 50

# ============================================
# PART 10: THE EXAM CHECKLIST
# ============================================
# 1. Create backup.service (Type=oneshot, ExecStart).
# 2. Create backup.timer (OnCalendar, Persistent).
# 3. [Install] WantedBy=timers.target.
# 4. systemd-analyze verify backup.timer.
# 5. systemctl daemon-reload.
# 6. systemctl enable --now backup.timer.
# 7. Verify: systemctl list-timers backup.timer.
# The service is NOT enabled; the timer is.

Quick Reference

Timer vs Service

FilePurposeEnable Target
backup.serviceDefines the command (ExecStart)Not enabled directly
backup.timerDefines the scheduleenable --now backup.timer

Scheduling Directives

DirectiveTypeExample
OnCalendar=Real-time*-*-* 03:00:00
OnBootSec=Monotonic5min
OnUnitActiveSec=Monotonic24h
OnActiveSec=Monotonic1h

Common OnCalendar= Patterns

NeedExpression
Daily 3 AM*-*-* 03:00:00
Weekdays 10 AMMon..Fri *-*-* 10:00:00
Every 15 min*-*-* *:00/15:00
Monthly 1st*-*-01 00:00:00

Verification Commands

CommandPurpose
systemd-analyze verify <timer>Check syntax
systemd-analyze calendar "<expr>"Validate schedule
systemctl list-timers --allNext/last trigger times
journalctl -u <service>Service output

Best Practices

✅ Do This:

# Use timers.target in [Install].                            // ✅
[Install]
WantedBy=timers.target

# Set Persistent=true for backups/maintenance.               // ✅
Persistent=true

# Use Type=oneshot for timer-triggered services.             // ✅
[Service]
Type=oneshot

# Verify with systemd-analyze before enabling.               // ✅
systemd-analyze verify backup.timer

❌ Don’t Do This:

# Enable the service instead of the timer.                   // ❌
systemctl enable backup.service  # Never runs on schedule.

# Use multi-user.target for timer [Install].                 // ❌
WantedBy=multi-user.target  # Timer not activated by timers.target.

# Omit Persistent=true for critical jobs.                    // ❌
# Missed runs are silently skipped.

# Trust a timer without verification.                        // ❌
# Misspelled directives are silently ignored.

Common Pitfalls

PitfallWhy It HappensFix
Timer never firesEnabled .service, not .timerenable --now <name>.timer
Missed run after rebootPersistent not setPersistent=true
Weekday trigger ignoredMisspelled OnCalendar= with valid OnBootSec=systemd-analyze verify
Timer not active at boot[Install] uses multi-user.targetUse timers.target
Schedule not what expectedCalendar expression wrongsystemd-analyze calendar

Real-World Examples

1. Daily Backup at 3 AM

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

2. Every 15 Minutes

OnCalendar=*-*-* *:00/15:00

3. Weekdays at 10 AM

OnCalendar=Mon..Fri *-*-* 10:00:00

4. Run After Boot

OnBootSec=5min

5. Combined Trigger

OnBootSec=5min
OnUnitActiveSec=24h

6. Oneshot Service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/job.sh

7. Enable the Timer

sudo systemctl enable --now backup.timer

8. List Timers

systemctl list-timers --all

9. Verify Syntax

systemd-analyze verify backup.timer

10. Check Service Output

journalctl -u backup.service -n 50

Visual

Timer-Service Pair

┌──────────────────────────────────────────────┐
│  backup.timer         backup.service         │
│  (WHEN)               (WHAT)                 │
│       │                      │               │
│  OnCalendar=            ExecStart=           │
│  Persistent=            Type=oneshot         │
│       │                      │               │
│       └──── triggers ────────┘               │
│                                              │
│  Same base name = automatic pairing.         │
│  Unit= overrides if different name.          │
│                                              │
│  enable --now backup.timer                   │
│  NOT backup.service.                         │
│                                              │
└──────────────────────────────────────────────┘

Real-Time vs Monotonic

┌──────────────────────────────────────────────┐
│  REAL-TIME (OnCalendar)                      │
│    Fires at wall-clock time.                 │
│    "Every day at 3 AM."                      │
│    Depends on system clock.                  │
│                                              │
│  MONOTONIC (OnBootSec, OnUnitActiveSec)      │
│    Fires relative to an event.               │
│    "5 minutes after boot."                   │
│    "24 hours after last run."                │
│                                              │
│  Both can coexist in one [Timer].            │
│  Any condition triggers the service.         │
│                                              │
└──────────────────────────────────────────────┘

Persistent Behavior

┌──────────────────────────────────────────────┐
│  Persistent=false (default)                  │
│    3 AM scheduled. System off at 3 AM.       │
│    System boots at 8 AM.                     │
│    → No run. Missed forever.                 │
│                                              │
│  Persistent=true                             │
│    3 AM scheduled. System off at 3 AM.       │
│    System boots at 8 AM.                     │
│    → Run immediately after boot.             │
│                                              │
│  systemd stores last trigger timestamp.      │
│                                              │
└──────────────────────────────────────────────┘

Verification Flow

┌──────────────────────────────────────────────┐
│  BEFORE ENABLING                             │
│                                              │
│  systemd-analyze verify backup.timer         │
│    → Syntax OK?                              │
│                                              │
│  systemd-analyze calendar "*-*-* 03:00:00"   │
│    → Next elapse: correct?                   │
│                                              │
│  AFTER ENABLING                              │
│                                              │
│  systemctl list-timers backup.timer          │
│    → NEXT column populated?                  │
│    → ACTIVATES shows backup.service?         │
│                                              │
│  journalctl -u backup.service                │
│    → Service ran? Errors?                    │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Timer file<name>.timer
Service file<name>.service (same base name)
Enable targetenable --now <name>.timer
[Install] targetWantedBy=timers.target
Real-time schedulingOnCalendar=
Monotonic schedulingOnBootSec=, OnUnitActiveSec=
Missed run handlingPersistent=true
Load spreadingRandomizedDelaySec=
List timerssystemctl list-timers --all
Syntax validationsystemd-analyze verify

Key takeaways:

  • Timers are triggers; services are tasks. The .timer file schedules, the .service file executes. They share a base name, and the exam expects both files. Enabling the service instead of the timer produces a job that never runs on schedule .
  • OnCalendar= replaces cron with more expressive syntax. Mon..Fri *-*-* 10:00:00 runs weekdays at 10 AM. systemd-analyze calendar validates expressions before deployment .
  • Persistent=true catches missed runs. Without it, a timer due while the system was off does not fire after boot. With it, the missed run triggers immediately .
  • The [Install] target for timers is timers.target, not multi-user.target. This is the target that activates all timers at boot.
  • Malformed directives can be silently ignored. A misspelled OnClendar= with a valid OnBootSec= produces no error at runtime; only systemd-analyze verify catches it .
  • systemctl list-timers is the verification command. It shows the next trigger, the last trigger, and the service each timer activates.

Remember: A systemd timer is a scheduled trigger for a service. The .timer file says when; the .service file says what. They pair by name, and the timer is what gets enabled. OnCalendar= handles wall-clock schedules, OnBootSec= and OnUnitActiveSec= handle relative schedules, and Persistent=true ensures a missed run is not silently skipped. The exam grader checks that the timer is enabled and that the service actually runs on schedule. systemd-analyze verify before enable --now catches the syntax errors that would otherwise produce a timer that looks active but never fires.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!