Scheduling Tasks with systemd Timers
systemd timers replace cron for scheduling tasks on systemd-managed Linux systems. Where cron uses a single line with a five-field time expression, systemd timers use two unit files: one describing what to run and one describing when to run it. The tradeoff for this verbosity is full integration with the rest of systemd: structured logging, dependencies, randomisation, and the same management interface as every other systemd unit.
How timers work
A timer unit (.timer) activates a corresponding unit (usually a .service) when its time conditions are met. The timer and service share the same base name: backup.timer activates backup.service.
# List all active timers and their next scheduled run
systemctl list-timers
# List all timers including inactive ones
systemctl list-timers --all
# Example output
# NEXT LEFT LAST PASSED UNIT ACTIVATES
# Sun 2026-06-29 02:40:00 UTC 14h left Sat 2026-06-28 02:40:01 UTC 9h ago logrotate.timer logrotate.service
# Sun 2026-06-29 00:00:00 UTC 12h left Sat 2026-06-28 00:00:05 UTC 12h ago apt-daily-upgrade.timer apt-daily-upgrade.service
Timer unit anatomy
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup every day at 2 AM
Requires=backup.service
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
The [Timer] section is what makes a timer unit different from any other unit type.
Time specification: OnCalendar
OnCalendar= specifies an absolute schedule using calendar time expressions. The format is:
DayOfWeek Year-Month-Day Hour:Minute:Second
Any component can be * (any) or omitted. Multiple values are separated by spaces or commas.
# Every day at midnight
OnCalendar=daily
OnCalendar=*-*-* 00:00:00 # equivalent long form
# Every hour
OnCalendar=hourly
OnCalendar=*-*-* *:00:00 # equivalent
# Every Monday at 2:30 AM
OnCalendar=Mon *-*-* 02:30:00
# Every weekday at 9 AM
OnCalendar=Mon..Fri *-*-* 09:00:00
# Every Saturday and Sunday at noon
OnCalendar=Sat,Sun *-*-* 12:00:00
# First of every month at 6 AM
OnCalendar=*-*-01 06:00:00
# Every 15 minutes
OnCalendar=*:0/15
# Every 5 minutes
OnCalendar=*:0/5
# January 1st at midnight every year
OnCalendar=*-01-01 00:00:00
# Three times a day: at 8 AM, 1 PM, and 6 PM
OnCalendar=*-*-* 08,13,18:00:00
# Weekly on Sunday
OnCalendar=weekly
OnCalendar=Sun *-*-* 00:00:00 # equivalent
Validating calendar expressions
# Check that an expression is valid and see the next 5 trigger times
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
systemd-analyze calendar --iterations=5 "*:0/15"
Time specification: relative triggers
For tasks that should run relative to boot or relative to the last run:
# Run 5 minutes after the system boots
OnBootSec=5min
# Run 1 hour after the last activation of this service
OnUnitActiveSec=1h
# Run 30 seconds after systemd starts (before most services)
OnStartupSec=30sec
# Example: a watchdog that checks a service every 10 minutes after boot
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
Time unit suffixes
us microseconds
ms milliseconds
s seconds
min minutes
h hours
d days
w weeks
month months
year years
You can combine them: 1h 30min, 2d 6h.
The Persistent option
# Run missed activations on the next boot
Persistent=true
Without Persistent=true, if the system is off when a timer would fire, the run is simply skipped. With Persistent=true, systemd records the last activation time in /var/lib/systemd/timers/. On the next boot, if a run was missed, the service activates shortly after startup.
This is equivalent to what anacron does for cron jobs. Use Persistent=true for daily or weekly tasks where missing a run matters: database backups, log archiving, certificate renewal.
Randomising timer activation
# Add a random delay of up to 5 minutes
RandomizedDelaySec=5min
When many systems run the same timer at exactly the same time (all at midnight, or all at the top of the hour), the combined load can spike servers. RandomizedDelaySec adds a random delay, distributing activation across a window. The actual trigger time is unpredictable but within the specified window. Combined with OnCalendar=daily, this means the task runs sometime in the first 5 minutes after midnight, spread randomly.
# See the actual (randomised) next trigger time
systemctl list-timers
Creating a timer: step by step
1. Write the service unit
The service unit describes the command to run. Use Type=oneshot for tasks that run and exit.
# /etc/systemd/system/backup.service
[Unit]
Description=Daily system backup
After=network.target
[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/bin/backup.sh
StandardOutput=journal
StandardError=journal
Note that a oneshot service unit does not need an [Install] section — it is activated by the timer, not directly enabled.
2. Write the timer unit
# /etc/systemd/system/backup.timer
[Unit]
Description=Run daily backup at 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=15min
[Install]
WantedBy=timers.target
3. Enable and start the timer
# Reload systemd to pick up the new files
sudo systemctl daemon-reload
# Enable the timer (configures it to start at boot)
# and start it now (so it is active immediately)
sudo systemctl enable --now backup.timer
# Verify it is scheduled
systemctl list-timers | grep backup
# Check the timer's status
systemctl status backup.timer
# Run the service immediately for testing (without waiting for the timer)
sudo systemctl start backup.service
# Follow the service output
journalctl -fu backup.service
Managing timers
# See all timers
systemctl list-timers --all
# Enable a timer (will activate on future boots)
sudo systemctl enable backup.timer
# Start a timer now (activates for this session)
sudo systemctl start backup.timer
# Stop a running timer
sudo systemctl stop backup.timer
# Disable a timer (removes it from boot)
sudo systemctl disable backup.timer
# Check timer status
systemctl status backup.timer
# See when it last ran and next will run
systemctl show backup.timer --property=LastTriggerUSec
systemctl show backup.timer --property=NextElapseUSecRealtime
# Trigger the associated service right now (useful for testing)
sudo systemctl start backup.service
Viewing logs
This is where systemd timers have a clear advantage over cron: output goes to the journal automatically.
# Logs for the service (all runs)
journalctl -u backup.service
# Follow live
journalctl -fu backup.service
# Last 20 lines
journalctl -u backup.service -n 20
# Logs from today only
journalctl -u backup.service --since today
# Logs for both the timer and service
journalctl -u backup.timer -u backup.service
# See timer activation events
journalctl -u backup.timer
Real-world examples
Certificate renewal
# /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Renew Let's Encrypt certificates twice daily
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/certbot-renew.service
[Unit]
Description=Renew Let's Encrypt certificates
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet
Database backup
# /etc/systemd/system/postgres-backup.timer
[Unit]
Description=PostgreSQL backup every night at 3 AM
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/postgres-backup.service
[Unit]
Description=PostgreSQL nightly backup
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/pg-backup.sh
StandardOutput=journal
StandardError=journal
On-boot delay task
# /etc/systemd/system/startup-notify.timer
[Unit]
Description=Notify admin 2 minutes after boot
[Timer]
OnBootSec=2min
[Install]
WantedBy=timers.target
# /etc/systemd/system/startup-notify.service
[Unit]
Description=Send boot notification
[Service]
Type=oneshot
ExecStart=/usr/local/bin/send-boot-notification.sh
Timer vs cron comparison
| Feature | systemd Timer | cron |
|---|---|---|
| Logging | journal (queryable, structured) | syslog or email |
| Dependencies | full systemd dependency graph | none |
| Randomisation | RandomizedDelaySec | not built-in |
| Missed runs | Persistent=true | anacron needed |
| Management | systemctl | crontab |
| Syntax | two unit files | single line |
| User timers | yes (systemctl —user) | yes (user crontabs) |
| Portability | systemd systems only | any Unix |
| Debugging | journalctl -u service | grep CRON /var/log/syslog |
For new system-level scheduled tasks on a modern Linux server, systemd timers are worth the extra setup. For simple user-level tasks, or tasks that need to run on multiple different Linux systems including some without systemd, cron remains the practical choice.
Quick reference
# List timers
systemctl list-timers
systemctl list-timers --all
# Manage
sudo systemctl enable --now name.timer
sudo systemctl disable --now name.timer
systemctl status name.timer
# Test the service immediately
sudo systemctl start name.service
# Logs
journalctl -fu name.service
journalctl -u name.service --since today
# Validate a calendar expression
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
systemd-analyze calendar --iterations=5 "daily"
# After creating or editing unit files
sudo systemctl daemon-reload
Frequently Asked Questions
What is a systemd timer?
A systemd timer is a unit file with the .timer extension that activates another unit (usually a .service unit) at a specified time or interval. Timers are the systemd equivalent of cron jobs. Each timer is paired with a service unit of the same name: backup.timer activates backup.service. The timer defines when to run; the service defines what to run. Timers are managed with systemctl like any other unit and their output goes to the journal.
What is the difference between a systemd timer and a cron job?
Both schedule tasks to run at specified times, but systemd timers have several advantages. Output goes to the journal, so you can query it with journalctl. Timers can depend on other systemd units, so a task only runs after networking is up or a filesystem is mounted. The RandomizedDelaySec option staggers jobs that would otherwise all start at midnight. Missed runs when the system is off can be caught up with Persistent=true. On the other hand, cron has a simpler syntax, is available on any Unix system, and is better for user-level scheduling without systemd involvement.
What is the difference between OnCalendar and OnBootSec?
OnCalendar specifies an absolute schedule using calendar expressions like “daily”, “Mon --* 02:30:00”, or “weekly”. OnBootSec specifies a delay relative to the system boot, like OnBootSec=5min (run 5 minutes after boot). OnUnitActiveSec adds a delay relative to the last time the service was activated. OnCalendar is used for tasks that should run at specific clock times (backups, reports). OnBootSec and OnUnitActiveSec are used for tasks tied to system events or service cycles rather than wall-clock time.
How do I see when my systemd timers will next run?
Run systemctl list-timers to see all active timers with their last trigger time and next scheduled time. systemctl list-timers —all shows inactive timers too. For a specific timer, systemctl status timername.timer shows the next trigger and recent log output. The times are shown in the local timezone and include how long until the next activation.
What does Persistent=true do in a systemd timer?
Persistent=true makes the timer remember the last time it triggered (stored in /var/lib/systemd/timers/). If the system was off when the timer was supposed to fire, it will activate the service shortly after the next boot to make up for the missed run. This is the systemd equivalent of anacron behaviour. Without Persistent=true, missed runs are simply skipped. Persistent=true is most useful for timers using OnCalendar that should not miss daily or weekly maintenance tasks.