Schedule Tasks with systemd Timers in Linux

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:

text
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:

bash
sudo tee /usr/local/bin/write-time.sh <<'EOF'
#!/bin/bash
/usr/bin/date '+%F %T %Z' >> /var/log/write-time.log
EOF

Make the script executable:

bash
sudo chmod 755 /usr/local/bin/write-time.sh

Define the service unit:

ini
[Unit]
Description=Write timestamp to log

[Service]
Type=oneshot
ExecStart=/usr/local/bin/write-time.sh

Install it on the host:

bash
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
EOF

Type=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.timerwrite-time.service), systemd links them automatically. You do not need Unit= in that case.

ini
[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:

bash
sudo tee /etc/systemd/system/write-time.timer <<'EOF'
[Unit]
Description=Run write-time daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
EOF

Load and Start the Timer

Reload systemd after creating or editing units under /etc/systemd/system:

bash
sudo systemctl daemon-reload

Enable the timer and start it immediately:

bash
sudo systemctl enable --now write-time.timer
output
Created 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:

bash
systemctl is-active write-time.timer
output
active

The NEXT column shows when the calendar schedule fires:

bash
systemctl list-timers write-time.timer
output
NEXT                        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:

bash
sudo systemctl start write-time.service

Check that the log file received a line:

bash
sudo tail -1 /var/log/write-time.log
output
2026-08-08 17:45:33 IST

Read the service journal when you need more detail than the log file:

bash
journalctl -u write-time.service --no-pager -n 4
output
Aug 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:

bash
systemd-analyze calendar 'daily'
output
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 left

Common 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:

bash
systemd-analyze calendar '*-*-* 02:30:00'
output
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 left

Weekday morning schedule:

bash
systemd-analyze calendar 'Mon..Fri 08:00'
output
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 left

If 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:

ini
[Unit]
Description=Run write-time after boot then hourly

[Timer]
Unit=write-time.service
OnBootSec=5min
OnUnitActiveSec=1h

[Install]
WantedBy=timers.target

Install the monotonic timer:

bash
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
EOF

Reload and enable it:

bash
sudo systemctl daemon-reload

Start the monotonic timer at boot:

bash
sudo systemctl enable --now write-time-boot.timer

List the next monotonic trigger:

bash
systemctl list-timers write-time-boot.timer
output
NEXT                         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:

ini
RandomizedDelaySec=15min

systemd 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:

ini
AccuracySec=1s

Smaller 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:

bash
systemctl status write-time.timer --no-pager | head -10
output
● 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:

bash
systemctl status write-time.service --no-pager | head -14
output
○ 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:

bash
systemctl list-timers --all | grep write-time
output
Sat 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.service

write-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:

bash
sudo sed -i 's/OnCalendar=daily/OnCalendar=hourly/' /etc/systemd/system/write-time.timer

Pick up the new unit definitions:

bash
sudo systemctl daemon-reload

Restart the timer so the new schedule applies:

bash
sudo systemctl restart write-time.timer

Confirm the next run moved to the top of the hour:

bash
systemctl list-timers write-time.timer
output
NEXT                         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


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.


Frequently Asked Questions

1. What is the difference between a systemd timer and a systemd service?

A timer unit schedules when work should run. It activates a separate service unit that contains ExecStart and runs the command or script. The timer does not replace the service; both files are required for a scheduled job.

2. Do I need to restart systemd after editing a timer unit?

Run systemctl daemon-reload after you change any unit file under /etc/systemd/system, then restart or re-enable the timer so systemd picks up the new schedule. Forgetting daemon-reload is a common reason a timer keeps the old timing.

3. When should I use OnCalendar versus OnBootSec or OnUnitActiveSec?

OnCalendar follows wall-clock time such as daily at midnight or weekdays at 08:00. OnBootSec and OnUnitActiveSec are monotonic timers measured from boot or from the last activation of the unit named in Unit=, which suits boot delays and repeating intervals that are not tied to clock time.

4. Why does my timer show active but the script never runs?

A timer can be enabled without being started, or the timer name may not match the service it should trigger. Check systemctl list-timers, confirm Unit= in the timer when names differ, and read journalctl -u the service unit because timer status alone does not prove the command succeeded.

5. Does Persistent=true make every timer run after a reboot?

Persistent=true applies to calendar timers configured with OnCalendar=. It does not add catch-up behavior to monotonic timers. It is not a substitute for enable and start on the timer unit.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)