Timers
A systemd timer is a unit that activates another unit on a schedule. It is the systemd-native replacement for cron: instead of a crontab line, you write a .timer file that says when, paired with a .service file that says what. The timer never runs your command directly — it starts the matching service, and the service is where the command, the user, the environment, and the resource limits live. The pairing is by name: backup.timer activates backup.service unless you override the target with Unit=.
The operational payoff is that scheduled jobs become first-class units. They show up in systemctl list-timers, their output and exit status land in the journal instead of an emailed stdout, they inherit cgroup accounting and sandboxing, and a job that overruns its interval will not stack a second copy on top of the first the way two overlapping cron runs do. The cost is verbosity: every job is now two files instead of one crontab line, and the timer syntax has its own calendar grammar you have to learn.
The Timer and Service Pair
A timer unit carries a [Timer] section; the work goes in a sibling service. You enable and start the timer, not the service — the service is started on demand when the timer fires. A minimal pair for a nightly backup looks like this on Debian or Ubuntu, with both files under /etc/systemd/system/.
# /etc/systemd/system/backup.service [Unit] Description=Nightly backup [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh User=backup
# /etc/systemd/system/backup.timer [Unit] Description=Run the nightly backup [Timer] OnCalendar=*-*-* 02:30:00 Persistent=true [Install] WantedBy=timers.target
The service is Type=oneshot because it runs to completion and exits rather than staying resident — that is the right type for batch work, and it lets systemd treat the run as finished only when the script returns. There is no [Install] section in the service, because you never enable it yourself; the timer pulls it in. Enable the timer with systemctl enable --now backup.timer, and it both starts now and survives reboot through timers.target.
Calendar Timers with OnCalendar
Calendar timers fire at wall-clock times using OnCalendar=, which is the direct analogue of a cron schedule. The grammar is DayOfWeek Year-Month-Day Hour:Minute:Second, with * as a wildcard, comma lists, ranges with .., and / for repetition. There are also named shortcuts: daily, weekly, monthly, and hourly expand to fixed times.
| OnCalendar expression | Fires |
|---|---|
*-*-* 02:30:00 | Every day at 02:30 |
Mon..Fri 09:00 | Weekdays at 09:00 |
*-*-* *:0/15 | Every 15 minutes, on the quarter-hour |
Mon *-*-* 00:00:00 | Every Monday at midnight |
*-*-01 04:00 | The first of every month at 04:00 |
Always validate an expression before you trust it. systemd-analyze calendar 'Mon..Fri 09:00' parses the string, normalizes it, and prints the next elapse time — add --iterations=N to see several — so you can confirm a job lands when you think it does instead of discovering at 02:30 that you wrote the wrong field. The same tool catches the classic mistake of a time that never matches.
Monotonic Timers and Boot-Relative Schedules
Not every job belongs on a wall clock. Monotonic timers fire relative to an event rather than a calendar date, using directives like OnBootSec= (time after boot), OnStartupSec= (time after systemd itself started), OnUnitActiveSec= (time after the paired unit last ran), and OnUnitInactiveSec=. These are how you express "fifteen minutes after boot, then every six hours" without pinning to specific clock times.
[Timer] # run 15 min after boot, then every 6 h afterwards OnBootSec=15min OnUnitActiveSec=6h
Monotonic timers do not understand Persistent= — that directive only applies to OnCalendar timers, because "did I miss a run" only has meaning against a wall clock. Mixing the two in one timer is legal and occasionally useful, but the common pattern is to pick one model per job: calendar timers for "at 02:30 nightly", monotonic timers for "a while after boot, then periodically".
Catch-up, Jitter, and Accuracy
Persistent=true records the last time a calendar timer ran in /var/lib/systemd/timers/. If the machine was off or asleep when the timer should have fired, the job runs once at next boot to catch up. This is the systemd answer to anacron, and it is the directive laptops and intermittently-on servers need — without it, a backup scheduled for 02:30 simply never happens on a machine that is powered down overnight.
RandomizedDelaySec= spreads a timer's fire time randomly across a window, which matters when a fleet of hosts all run the same calendar timer and would otherwise hammer a backup target or package mirror at exactly 02:30. AccuracySec= controls how loosely systemd is allowed to coalesce timer wakeups — the default is one minute, which lets the kernel batch wakeups and save power; set it to 1s only when a job genuinely needs second-level precision, because tighter accuracy means more wakeups.
Inspecting and Debugging Timers
systemctl list-timers is the single most useful command here: it lists every active timer with its next elapse time, the time left, the last time it ran, and the unit it activates. systemctl list-timers --all includes inactive ones. To see why a specific job did or did not run, read the journal for the service, not the timer, since the service is where the command executed.
# what is scheduled and when it next fires systemctl list-timers --all # did the last run succeed? full output of the job journalctl -u backup.service # trigger the job by hand, ignoring the schedule systemctl start backup.service
Starting the .service by hand runs the job immediately and is the correct way to test it — starting the .timer only arms the schedule, it does not run the work. On Red Hat and Fedora the commands are identical; the only divergence from Debian and Ubuntu is unit placement conventions and the surrounding package names, not the timer mechanism itself.
systemd timers — recurring jobs that need logging, resource limits, dependency ordering, or catch-up after downtime. Choose them for anything you would otherwise babysit, and on any host you already manage with systemd units.
cron — one-line recurring jobs where portability and brevity win, and on systems without systemd. Still the fastest thing to write, but it has no overlap protection, no native logging beyond mailed stdout, and no missed-run catch-up unless you add anacron.
at — a single job at one future time ("run this once at 18:00"). Neither cron nor a timer is the right tool for a one-shot deferred command; at is.
- Enabling the
.serviceinstead of the.timer. Enabling the service either fails (oneshot with no install target) or runs it once at boot and never again — the schedule lives in the timer, and the timer is what youenable --now. - Omitting
Persistent=trueon a calendar timer for a laptop or an overnight-off server. The job silently never runs because the machine was powered down when the timer should have fired, and nothing logs the miss. - Naming the timer and service differently without an explicit
Unit=.nightly.timerlooks fornightly.service; if your work is inbackup.servicethe timer activates a unit that does not exist and fails quietly. - Editing a unit file and forgetting
systemctl daemon-reload. systemd keeps running the old cached definition, so your schedule change appears to have no effect until the next reboot. - Rolling out one
OnCalendar=02:30timer across a whole fleet with noRandomizedDelaySec=. Every host fires at the same instant and overwhelms the shared backup target or mirror. - Setting
AccuracySec=1sby reflex. Tight accuracy defeats wakeup coalescing and wastes power for jobs that do not care whether they run at 02:30:00 or 02:30:45. - Looking for failures in
journalctl -u backup.timer. The timer log only shows arming and firing; the command's actual output and exit status are under the.serviceunit.
- Validate every schedule with
systemd-analyze calendar 'expression'before deploying — it prints the next elapse time (use--iterations=Nfor more) and catches expressions that never match. - Set
Persistent=trueon any calendar timer that must not skip a run, so a missed window is caught up at next boot. - Add
RandomizedDelaySec=to any timer you deploy across more than one host, sized to spread load over minutes so the fleet does not fire in lockstep. - Keep the timer and service names matched so the implicit pairing works, and use
Unit=explicitly only when you deliberately want one timer to drive a differently-named service. - Make the service
Type=oneshotfor batch jobs, and test by runningsystemctl start the.servicedirectly before arming the timer. - Run
systemctl daemon-reloadafter every unit-file edit, then confirm the new schedule withsystemctl list-timers. - Sandbox scheduled jobs in the service with directives like
User=,ProtectSystem=strict, andPrivateTmp=true— a timer-driven service gets the same hardening surface as any other unit, which a crontab line never had.
anacron adds the missed-run catch-up that Persistent=true gives a timerWindows Task Scheduler — registered tasks with triggers, conditions, and history, the closest model to timer+servicemacOS launchd — .plist agents and daemons with StartCalendarInterval and StartInterval keysKnowledge Check
Why does a systemd timer use two unit files instead of one?
- The
.timerdefines when to fire and the paired.servicedefines what to run, so the job inherits service features like logging, resource limits, and sandboxing - The
.timerfile holds the command to run and the paired.servicefile holds the schedule, deliberately mirroring the way a single crontab line is internally split - One file is for wall-clock calendar schedules and the second file is required separately for monotonic ones
- systemd needs one separate unit file for each distinct user that the scheduled job will run as
A backup timer is set to OnCalendar=*-*-* 02:30 on a laptop that is powered off overnight. What does Persistent=true change?
- If the machine was off at 02:30, the job runs once at next boot to catch up the missed run
- It keeps the timer armed across reboots but does nothing about runs missed while powered off
- It runs the job every boot in addition to the scheduled time
- It converts the calendar timer into a monotonic
OnBootSectimer
When would you reach for at rather than a systemd timer or cron?
- For a single command to run once at one future time, with no recurrence
- For a recurring job that also needs missed-run catch-up after the host has been down
- For a fleet-wide job that must spread its load out with a randomized startup delay
- For any job whose stdout and exit status must be captured in the journal
A scheduled job is not producing output. Where do you look first?
journalctl -u job.service— the service is where the command ran and its exit status is recordedjournalctl -u job.timer— the timer's own log is where the command's stdout and exit status are recordedsystemctl status job.timer, which prints the full command output inline alongside the schedule- The mailbox of the user who owns the job, where systemd mails the captured stdout by default
Why add RandomizedDelaySec= to a calendar timer deployed across many hosts?
- It spreads each host's fire time across a window so the fleet does not hit a shared backup target or mirror simultaneously
- It tightens overall timer accuracy so that every host in the fleet fires at the exact same instant
- It is a strictly required prerequisite for
Persistent=trueto be able to catch up any of the runs that were missed across reboots - It retries the failed job at random intervals afterward until it finally succeeds
You got correct