| Tested on | RHEL 10.2 (Coughlan) — vm1.lab.example |
|---|---|
| Package | systemd 257-23.el10_2.2 |
| Applies to | Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora, Arch Linux |
| Privilege | sudo or root to create unit files, enable timers, and inspect system journals |
| Scope | Timer and service unit pairs, calendar and monotonic schedules, OnCalendar expressions, Persistent, RandomizedDelaySec and AccuracySec, enable and list-timers, journal inspection, safe edits, and comparison with cron. Does not cover full custom service authoring, anacron, or at-job scheduling. |
| Related guides | systemctl command crontab command at command Run script at boot without cron RHCSA tutorial |
A systemd timer is not a cron line hidden inside a different file format. A timer unit decides when work should run and activates a service unit that actually runs your command or script. Once you see that split, OnCalendar, list-timers, and journalctl all follow the same pattern.
This walkthrough builds a small timestamp logger on vm1.lab.example: one shared service unit, a daily calendar timer, and a separate monotonic timer that fires after boot and then on an hourly interval from the last service activation.
How systemd Timers Work
systemd separates scheduling from execution:
write-time.timer
↓ activates on schedule
write-time.service
↓ runs
/usr/local/bin/write-time.sh| Piece | Role |
|---|---|
Timer unit (*.timer) |
Defines calendar or monotonic triggers; starts the paired service when a trigger fires |
Service unit (*.service) |
Holds ExecStart, environment, user context, and logging for the actual command |
| Calendar timer | Wall-clock schedule via OnCalendar= (daily, weekdays at 08:00, and similar) |
| Monotonic timer | Intervals from an event such as boot (OnBootSec=) or from the last activation of the unit in Unit= (OnUnitActiveSec=) |
The timer watches the clock or elapsed time. The service runs the job once per activation. For recurring work the service is usually Type=oneshot so each timer firing starts a fresh short-lived unit.
systemd Timer Quick Reference
| Task | Command |
|---|---|
| List active timers | systemctl list-timers |
| Include inactive timers | systemctl list-timers --all |
| Start timer now | systemctl start example.timer |
| Enable at boot | systemctl enable example.timer |
| Enable and start | systemctl enable --now example.timer |
| Inspect timer | systemctl status example.timer |
| Next run for one timer | systemctl list-timers example.timer |
| Test calendar expression | systemd-analyze calendar 'daily' |
| Reload unit files | systemctl daemon-reload |
| Service logs | journalctl -u example.service |
Create the Service Unit
Put the command in the service unit. The timer file should schedule; it should not embed ExecStart for your script.
Create a script that appends the current time to a log file:
sudo tee /usr/local/bin/write-time.sh <<'EOF'
#!/bin/bash
/usr/bin/date '+%F %T %Z' >> /var/log/write-time.log
EOFMake the script executable:
sudo chmod 755 /usr/local/bin/write-time.shDefine the service unit:
[Unit]
Description=Write timestamp to log
[Service]
Type=oneshot
ExecStart=/usr/local/bin/write-time.shInstall it on the host:
sudo tee /etc/systemd/system/write-time.service <<'EOF'
[Unit]
Description=Write timestamp to log
[Service]
Type=oneshot
ExecStart=/usr/local/bin/write-time.sh
EOFType=oneshot fits scripts that run and exit. A long-running daemon would use Type=simple or Type=notify, but timers usually activate short jobs.
Use absolute paths in ExecStart. Relative paths break when systemd runs the unit from a minimal environment.
Create a Calendar Timer
When the timer basename matches the service basename (write-time.timer → write-time.service), systemd links them automatically. You do not need Unit= in that case.
[Unit]
Description=Run write-time daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target| Field | Meaning |
|---|---|
OnCalendar=daily |
Wall-clock schedule; daily normalizes to midnight local time |
Persistent=true |
If a calendar activation was missed while powered off, run once when the timer becomes active again |
WantedBy=timers.target |
Enables the timer when you enable the unit so it starts on boot |
Install the timer unit:
sudo tee /etc/systemd/system/write-time.timer <<'EOF'
[Unit]
Description=Run write-time daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
EOFLoad and Start the Timer
Reload systemd after creating or editing units under /etc/systemd/system:
sudo systemctl daemon-reloadEnable the timer and start it immediately:
sudo systemctl enable --now write-time.timerCreated symlink '/etc/systemd/system/timers.target.wants/write-time.timer' → '/etc/systemd/system/write-time.timer'.Confirm the timer is waiting for its next trigger:
systemctl is-active write-time.timeractiveThe NEXT column shows when the calendar schedule fires:
systemctl list-timers write-time.timerNEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2026-08-09 00:00:00 IST 6h - - write-time.timer write-time.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.ACTIVATES confirms the timer will start write-time.service at NEXT.
Trigger the Service Manually
You can run the service once without waiting for the timer:
sudo systemctl start write-time.serviceCheck that the log file received a line:
sudo tail -1 /var/log/write-time.log2026-08-08 17:45:33 ISTRead the service journal when you need more detail than the log file:
journalctl -u write-time.service --no-pager -n 4Aug 08 17:45:33 vm1.lab.example systemd[1]: Starting write-time.service - Write timestamp to log...
Aug 08 17:45:33 vm1.lab.example systemd[1]: write-time.service: Deactivated successfully.
Aug 08 17:45:33 vm1.lab.example systemd[1]: Finished write-time.service - Write timestamp to log.Finished with status=0/SUCCESS means the script ran. A timer that shows active (waiting) only proves scheduling, not that the last job succeeded.
Understand OnCalendar
OnCalendar uses a structured time syntax. Test expressions before you deploy them:
systemd-analyze calendar 'daily'Original form: daily
Normalized form: *-*-* 00:00:00
Next elapse: Sun 2026-08-09 00:00:00 IST
(in UTC): Sat 2026-08-08 18:30:00 UTC
From now: 6h leftCommon patterns:
| Expression | Normalized example | Use |
|---|---|---|
hourly |
*-*-* *:00:00 |
Every hour on the hour |
daily |
*-*-* 00:00:00 |
Once per day at midnight |
*-*-* 02:30:00 |
same | Specific local time daily |
Mon..Fri 08:00 |
Mon..Fri *-*-* 08:00:00 |
Weekdays at 08:00 |
Sat 09:00 |
Sat *-*-* 09:00:00 |
Saturdays at 09:00 |
*-*-15 09:00:00 |
same | 15th of each month at 09:00 |
Verify a specific time:
systemd-analyze calendar '*-*-* 02:30:00'Normalized form: *-*-* 02:30:00
Next elapse: Sun 2026-08-09 02:30:00 IST
(in UTC): Sat 2026-08-08 21:00:00 UTC
From now: 8h leftWeekday morning schedule:
systemd-analyze calendar 'Mon..Fri 08:00'Original form: Mon..Fri 08:00
Normalized form: Mon..Fri *-*-* 08:00:00
Next elapse: Mon 2026-08-10 08:00:00 IST
(in UTC): Mon 2026-08-10 02:30:00 UTC
From now: 1 day 14h leftIf systemd-analyze calendar fails to parse an expression, fix the string before you reload the timer.
Create a Monotonic Timer
Monotonic timers count elapsed time, not wall-clock time. They suit delays after boot or fixed intervals between runs.
| Setting | Starts counting from |
|---|---|
OnBootSec= |
System boot |
OnStartupSec= |
systemd manager startup |
OnUnitActiveSec= |
Last time the unit named in Unit= entered active state |
OnUnitInactiveSec= |
Last time the activated unit left active state |
Example goal: run five minutes after boot, then one hour after the last activation of write-time.service.
When the timer name differs from the service name, set Unit= explicitly:
[Unit]
Description=Run write-time after boot then hourly
[Timer]
Unit=write-time.service
OnBootSec=5min
OnUnitActiveSec=1h
[Install]
WantedBy=timers.targetInstall the monotonic timer:
sudo tee /etc/systemd/system/write-time-boot.timer <<'EOF'
[Unit]
Description=Run write-time after boot then hourly
[Timer]
Unit=write-time.service
OnBootSec=5min
OnUnitActiveSec=1h
[Install]
WantedBy=timers.target
EOFReload and enable it:
sudo systemctl daemon-reloadStart the monotonic timer at boot:
sudo systemctl enable --now write-time-boot.timerList the next monotonic trigger:
systemctl list-timers write-time-boot.timerNEXT LEFT LAST PASSED UNIT ACTIVATES
Sat 2026-08-08 18:45:37 IST 59min Sat 2026-08-08 17:45:37 IST 67ms ago write-time-boot.timer write-time.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.If the system booted long before you enabled the timer, OnBootSec may fire immediately because boot plus five minutes already passed. NEXT then reflects OnUnitActiveSec for the following hour.
OnUnitActiveSec= measures from the last activation of the unit in Unit=, not from the last run triggered by this particular timer. Because this lab deliberately shares write-time.service between two timers, a manual systemctl start write-time.service or an activation from write-time.timer also updates the reference point used by OnUnitActiveSec=1h on write-time-boot.timer. Use a separate service unit if the monotonic schedule must be independent.
Calendar timers fire at clock times. Monotonic timers fire relative to boot or prior activations of the named unit, so they do not replace cron-style schedules.
Run Missed Jobs with Persistent=true
Persistent=true applies only to timers configured with OnCalendar=. If the timer was inactive when one or more calendar events were missed, systemd can trigger the service once when the timer becomes active again.
Typical case:
- The system was off when a scheduled wall-clock time arrived
- After boot, when the calendar timer becomes active, systemd runs the associated service once for the most recent missed calendar activation
It does not mean “run on every boot regardless of schedule.” It has no effect on OnBootSec=, OnUnitActiveSec=, or other monotonic timers.
Add Randomized Delay
Many hosts sharing one schedule can stampede a server at the same second. Spread activations with:
RandomizedDelaySec=15minsystemd adds a random delay up to fifteen minutes after each scheduled time. Combine with OnCalendar on fleet maintenance jobs.
RandomizedDelaySec= intentionally spreads jobs out. It is not the same as AccuracySec=, which controls how closely systemd wakes timers to the scheduled instant.
Control Timer Accuracy
AccuracySec= defaults to 1min. systemd may coalesce timer wakeups within that window for efficiency rather than waking at the exact second.
If a job genuinely needs tighter timing, reduce the value:
AccuracySec=1sSmaller values wake the system closer to the exact OnCalendar time. Very small values increase timer interrupts; use them only when the job truly needs tight timing.
AccuracySec= and RandomizedDelaySec= serve different purposes: accuracy allows batching near the scheduled time, while randomized delay deliberately spreads activations across a window.
Inspect Timer and Service Results
Always inspect both the timer and the activated service. At this point both write-time.timer and write-time-boot.timer are enabled, but the calendar timer still uses OnCalendar=daily and has not fired on its own schedule yet.
Timer status shows scheduling state:
systemctl status write-time.timer --no-pager | head -10● write-time.timer - Run write-time daily
Loaded: loaded (/etc/systemd/system/write-time.timer; enabled; preset: disabled)
Active: active (waiting) since Sat 2026-08-08 17:50:38 IST; 9s ago
Invocation: 5943e83261a8490aa947780c3c9ff409
Trigger: Sun 2026-08-09 00:00:00 IST; 6h left
Triggers: ● write-time.service
Aug 08 17:50:38 vm1.lab.example systemd[1]: Started write-time.timer - Run write-time daily.Trigger at midnight matches OnCalendar=daily. The calendar timer has not elapsed its schedule yet, so LAST in list-timers stays empty.
Service status shows whether the command succeeded:
systemctl status write-time.service --no-pager | head -14○ write-time.service - Write timestamp to log
Loaded: loaded (/etc/systemd/system/write-time.service; static)
Active: inactive (dead) since Sat 2026-08-08 17:50:41 IST; 6s ago
Invocation: efc69f920c6c4cadbe58d044ca85e3bf
TriggeredBy: ● write-time.timer
● write-time-boot.timer
Process: 56116 ExecStart=/usr/local/bin/write-time.sh (code=exited, status=0/SUCCESS)
Main PID: 56116 (code=exited, status=0/SUCCESS)
Mem peak: 1.2M
CPU: 25ms
Aug 08 17:50:41 vm1.lab.example systemd[1]: Starting write-time.service - Write timestamp to log...
Aug 08 17:50:41 vm1.lab.example systemd[1]: write-time.service: Deactivated successfully.
Aug 08 17:50:41 vm1.lab.example systemd[1]: Finished write-time.service - Write timestamp to log.TriggeredBy can list both timers when they share one service unit. status=0/SUCCESS on ExecStart is what you want.
List every loaded timer including disabled ones:
systemctl list-timers --all | grep write-timeSat 2026-08-08 18:50:41 IST 59min Sat 2026-08-08 17:50:41 IST 88ms ago write-time-boot.timer write-time.service
Sun 2026-08-09 00:00:00 IST 6h - - write-time.timer write-time.servicewrite-time.timer still points at midnight. write-time-boot.timer shows a LAST time because the monotonic timer already activated the shared service.
Modify an Existing Timer Safely
Edit the unit file or a drop-in, reload, and restart the timer so NEXT updates.
Change the calendar timer from daily to hourly:
sudo sed -i 's/OnCalendar=daily/OnCalendar=hourly/' /etc/systemd/system/write-time.timerPick up the new unit definitions:
sudo systemctl daemon-reloadRestart the timer so the new schedule applies:
sudo systemctl restart write-time.timerConfirm the next run moved to the top of the hour:
systemctl list-timers write-time.timerNEXT LEFT LAST PASSED UNIT ACTIVATES
Sat 2026-08-08 18:00:00 IST 9min - - write-time.timer write-time.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.NEXT moved to the top of the hour because OnCalendar=hourly normalized to *-*-* *:00:00. LAST stays empty because the calendar timer itself still has not fired; earlier runs came from manual start or write-time-boot.timer, not from write-time.timer.
Skipping daemon-reload leaves systemd using the old unit text even after you edit the file on disk.
systemd Timer vs cron
| Topic | systemd timer | cron |
|---|---|---|
| Unit pairing | Timer plus service; command lives in .service |
Single crontab line runs the command |
| Logging | journalctl -u with structured unit names |
Often redirected manually |
| Dependencies | Can order after network-online.target and other units |
No native unit graph |
| Missed wall-clock runs | Persistent=true on calendar timers |
anacron or manual catch-up |
| Resource limits | Service unit supports cgroup limits | Per-line limits are limited |
| Simplicity | Two files and daemon-reload |
One crontab entry is quick for simple jobs |
| Scope | System units under /etc/systemd/system; user units under ~/.config/systemd/user |
User crontab or /etc/cron.d |
Neither tool wins every scenario. cron stays fine for a single user crontab on one host. Timers integrate cleanly with systemd logging, dependencies, and service management already on modern Linux.
For cron syntax and crontab editing, see crontab command. For one-shot future jobs, see at command.
Troubleshoot systemd Timers
| Symptom | Likely cause | Fix |
|---|---|---|
| Timer enabled but never fires | Enabled without start, or not enabled for boot | systemctl enable --now example.timer; check list-timers |
| Wrong schedule | Bad OnCalendar string |
systemd-analyze calendar 'EXPRESSION'; fix unit; daemon-reload; restart timer |
| Service never starts | Timer and service names differ without Unit= |
Add Unit=example.service under [Timer] |
| Timer fires, no log output | Service fails or script path wrong | journalctl -u example.service; fix ExecStart; use absolute paths |
| Script works manually, fails in timer | Relative path or missing PATH |
Absolute paths in ExecStart; set Environment= if needed |
| Edited unit, old timing remains | No daemon-reload |
systemctl daemon-reload; restart timer |
| Timer missing after reboot | Timer not enabled | systemctl enable example.timer |
| Expected catch-up did not run | Persistent= only applies to OnCalendar= timers |
Use Persistent=true on calendar timers only; verify with list-timers after boot |
| User timer idle until login | User session not lingering | loginctl enable-linger username for user timers |
When the job silently does nothing, run the service manually with systemctl start example.service and read journalctl before you blame the schedule.
References
- systemd.timer(5) — Linux manual page
- systemd.service(5) — Linux manual page
- systemd.time(7) — Linux manual page
- Red Hat — Automating software updates in RHEL (DNF Automatic timers)
Summary
You scheduled work with systemd by pairing a timer unit with a service unit. The timer answers when the job should run; the service holds ExecStart and runs your script or command once per activation.
On this lab host you built write-time.service with Type=oneshot, attached a calendar timer with OnCalendar=daily and Persistent=true, and used systemctl enable --now, list-timers, and journalctl to confirm scheduling and execution. systemd-analyze calendar validates OnCalendar strings before they reach production. A separate monotonic timer with OnBootSec and OnUnitActiveSec repeats on elapsed time from boot or from the last activation of the unit in Unit=, and needs Unit= when the basename does not match the service file.
The pitfall to remember is checking only the timer: active (waiting) does not prove the script succeeded. Read journalctl -u on the service unit. Persistent=true applies only to OnCalendar= timers, not monotonic schedules. Sharing one service between multiple timers or manual starts shifts the OnUnitActiveSec= reference point. After any unit edit, run daemon-reload and restart the timer. cron remains simpler for one-line personal schedules; timers fit hosts already managed through systemd.

