systemd-timer: scheduling instead of cron
cron works, but its logs are flat text files with no structure, and service dependencies require workarounds like embedding Requires= logic inside shell scripts. systemd-timer fixes this: unified management interface, logs in journald, dependencies through the familiar After= and WantedBy= directives — all in one stack.
Structure: .service and .timer
A timer is a separate unit that triggers a .service. The separation is intentional: the service can be invoked manually or on a schedule.
backup.service is a regular unit, startable via systemctl start backup.service.
backup.timer is the trigger. Without it, the service will not run on a schedule.
Persistent=true — if the machine was off when the timer fired, it catches up after boot.
Calendar Timers (OnCalendar)
OnCalendar= format is the closest analog to cron expressions, but with different syntax.
| Example | Fires |
|---|---|
OnCalendar=daily | Every day at 00:00 |
OnCalendar=*-*-01 03:00 | First day of each month at 03:00 |
OnCalendar=*-*-* 02:00 | Every day at 02:00 |
OnCalendar=09..17:00 | Every hour from 09:00 to 17:00 |
OnCalendar=*:0/15 | Every 15 minutes |
OnCalendar=Mon..Fri 09:30 | Weekdays at 09:30 |
Multiple values can be specified comma-separated:
To validate syntax before applying:
Output shows the next firing date. Useful when “every second Tuesday” becomes *-*-1..31 03:00 — verify before deployment.
Monotonic Timers
Monotonic timers count from an event, not the clock.
| Directive | Fires |
|---|---|
OnBootSec=5min | 5 minutes after boot |
OnStartupSec=10min | 10 minutes after systemd manager starts |
OnUnitActiveSec=1h | 1 hour after the service last ran |
OnUnitInactiveSec=1d | 1 day after the service stopped |
OnBootSec and OnStartupSec are similar, but OnBootSec resets on each boot while OnStartupSec counts from when the systemd manager started. In practice, the difference shows in containers and during live migration.
Combination OnBootSec + OnUnitActiveSec implements “every hour, but not before boot”:
Verification: systemctl list-timers
After enabling and starting:
Without --all shows only active timers. NEXT is when it fires, LEFT is how long until then.
If the timer does not appear in the list — check status:
Common cause of a silent timer is a missing WantedBy=timers.target.
User-Level Timers (systemd –user)
Not every task needs root. Deployment scripts, home-directory cache cleanup, periodic git fetch — better under a user.
Units go in ~/.config/systemd/user/:
Activation:
User timers need linger if they should run without login:
Common Mistakes
Missing OnCalendar or monotonic timer. Without a [Timer] directive, the timer will never fire. systemctl start backup.timer starts the unit, but without a schedule it just sits there.
Forgot WantedBy. Without [Install], the unit does not persist across reboots. systemctl enable backup.timer completes without errors, but it will not appear in list-timers.
ExecStart in .timer. A timer only triggers its linked service. Placing ExecStart in .timer causes systemd to ignore it and pull from .service.
Persistent with no prior run. On first activation, Persistent=true does nothing — there is no “last run” in history. The service fires only at the next scheduled time.
Logging: cron vs timer in journald
Cron sends output to syslog or cron.log, structure is a text line with timestamp. Parsing requires grep or awk.
systemd-timer writes service stdout/stderr directly to journald:
Filtering by time, unit, severity — standard journalctl flags:
| Flag | Effect |
|---|---|
-u backup.service | This unit only |
-n 100 | Last 100 lines |
-f | Follow in real time |
--since "1 hour ago" | Time range filter |
-p err | Errors only |
Actual firing time is recorded in metadata. Build execution history without parsing text logs.
Output includes real start time, simplifying debugging of missed triggers.
Summary
Switching from cron to systemd-timer pays off when the workload already lives in a systemd environment. Unified management, dependencies via After=, logs in journald — gains are tangible. For a crontab one-liner, systemd-timer is overkill, but for scripts with dependencies, logging, and boot persistence — a mature tool.