systemd Timers vs Cron: When to Use Each (2026)
Last updated: March 2026
Cron is the classic Unix job scheduler. systemd timers are its modern alternative, built into the same init system that manages your services. Choosing between them depends on your requirements: cron is simpler for basic schedules, while systemd timers offer logging integration, dependency management, catch-up runs, and per-user control without additional tooling. This guide explains both, when to use each, and how to migrate a cron job to a systemd timer.
Feature Comparison
| Feature | cron | systemd timers |
|---|---|---|
| Logging | Writes to syslog/mail — easy to miss | Full journald integration, journalctl -u |
| Dependencies | None — always runs at scheduled time | Can depend on network, mounts, services |
| Missed runs (catch-up) | Not supported | Optional with Persistent=true |
| Per-user scheduling | crontab -e for any user |
User units in ~/.config/systemd/user/ |
| Randomized delay | Not built-in | RandomizedDelaySec= |
| CPU/memory limits | Not supported | Full cgroup resource control |
| Complex schedules | 5-field cron syntax | OnCalendar= expressions |
| Viewing scheduled jobs | crontab -l, no system-wide view |
systemctl list-timers |
| Activation by events | Not supported | OnBootSec, OnActiveSec, path units |
| Complexity | Low — one file | Medium — two files (.timer + .service) |
When to use cron: - Simple, recurring tasks on a single server. - Quick one-liners or scripts that already exist. - Environments where systemd is not available (containers, embedded). - You or your team are more comfortable with cron syntax.
When to use systemd timers:
- You need log visibility via journalctl.
- The job has dependencies (must run after network is up, database is ready, etc.).
- You want catch-up behavior if the machine was off during a scheduled run.
- You need resource limits (CPU, memory) on the scheduled job.
- You want to manage scheduled tasks the same way you manage services.
Cron Syntax Recap
A crontab entry has five time fields followed by the command:
# ┌───────── minute (0 - 59)
# │ ┌───────── hour (0 - 23)
# │ │ ┌───────── day of month (1 - 31)
# │ │ │ ┌───────── month (1 - 12)
# │ │ │ │ ┌───────── day of week (0 - 7, 0 and 7 = Sunday)
# │ │ │ │ │
# * * * * * command
Common examples:
# Every minute
* * * * * /usr/bin/myscript.sh
# Every day at 2:30 AM
30 2 * * * /usr/bin/backup.sh
# Every hour
0 * * * * /usr/bin/hourly-task.sh
# Every Monday at 9 AM
0 9 * * 1 /usr/bin/weekly-report.sh
# Every 15 minutes
*/15 * * * * /usr/bin/check-health.sh
Edit your crontab:
crontab -e # edit current user's crontab
crontab -l # list current user's crontab
sudo crontab -u www-data -e # edit another user's crontab
System-wide cron jobs go in /etc/cron.d/ or /etc/crontab.
systemd Timer Anatomy
A systemd timer consists of two files:
1. A .timer unit — defines when to run.
2. A .service unit — defines what to run.
The timer and service must have the same base name (e.g., mytask.timer activates mytask.service).
The .service file
# /etc/systemd/system/mytask.service
[Unit]
Description=My Scheduled Task
[Service]
Type=oneshot
User=myuser
ExecStart=/usr/local/bin/mytask.sh
Use Type=oneshot for tasks that run once and exit. RemainAfterExit=no (default) is correct for timers.
The .timer file
# /etc/systemd/system/mytask.timer
[Unit]
Description=Run My Task daily at 2 AM
Requires=mytask.service
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Enable and start the timer (not the service — the timer activates the service):
sudo systemctl daemon-reload
sudo systemctl enable --now mytask.timer
OnCalendar Examples
OnCalendar= uses a calendar event expression. Common formats:
| Expression | Meaning |
|---|---|
OnCalendar=daily |
Every day at midnight |
OnCalendar=weekly |
Every Monday at midnight |
OnCalendar=monthly |
First day of month at midnight |
OnCalendar=hourly |
Every hour at :00 |
OnCalendar=minutely |
Every minute |
OnCalendar=*:0/15 |
Every 15 minutes |
OnCalendar=Mon..Fri 09:00 |
Weekdays at 9 AM |
OnCalendar=*-*-* 02:30:00 |
Every day at 2:30 AM |
OnCalendar=2026-03-26 08:00:00 |
Specific date and time once |
OnCalendar=Mon *-*-* 08:00 |
Every Monday at 8 AM |
OnCalendar=*-*-01 00:00:00 |
1st of every month at midnight |
Validate an expression before using it:
systemd-analyze calendar "Mon..Fri 09:00"
# Output:
# Original form: Mon..Fri 09:00
# Normalized form: Mon..Fri *-*-* 09:00:00
# Next elapse: Mon 2026-03-30 09:00:00 UTC
# (in UTC): Mon 2026-03-30 09:00:00 UTC
# From now: 3 days 22h left
OnBootSec and OnUnitActiveSec
These directives trigger relative to an event, not a calendar time.
OnBootSec: Run a specified time after the system boots:
[Timer]
OnBootSec=5min
Runs the service 5 minutes after boot. Useful for post-boot initialization tasks.
OnUnitActiveSec: Run a specified time after the timer unit itself was last activated:
[Timer]
OnBootSec=10min
OnUnitActiveSec=1h
Runs 10 minutes after boot, then every 1 hour thereafter.
OnActiveSec: Time after the timer unit becomes active.
OnStartupSec: Time after systemd manager started (for user units).
You can combine OnBootSec and OnCalendar in the same timer:
[Timer]
OnCalendar=daily
OnBootSec=5min
Persistent=true
Persistent=true: Catch-Up Runs
If the machine is powered off during a scheduled run, cron silently skips it. With Persistent=true, systemd runs the job immediately on next boot if the last trigger was missed:
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
If the machine was off at 3 AM, the job runs as soon as it boots up. Useful for backups and cleanup tasks.
RandomizedDelaySec: Avoid Thundering Herd
If many machines run the same timer (e.g., via config management), add a random delay to spread the load:
[Timer]
OnCalendar=hourly
RandomizedDelaySec=10min
The job will run between :00 and :10 of each hour, randomly distributed. This prevents all servers from hitting a shared resource (database, API, NFS) simultaneously.
Listing Active Timers
systemctl list-timers
Example output:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-03-26 04:00:00 UTC 12h left Wed 2026-03-25 04:00:00 UTC 12h ago logrotate.timer logrotate.service
Thu 2026-03-26 09:30:00 UTC 18h left Wed 2026-03-25 09:30:00 UTC 7h ago mytask.timer mytask.service
2 timers listed.
Include inactive timers:
systemctl list-timers --all
Migration Example: cron → systemd Timer
Original cron entry
# /etc/cron.d/db-backup
30 2 * * * postgres /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1
Step 1: Create the service file
# /etc/systemd/system/db-backup.service
[Unit]
Description=Database Backup
After=postgresql.service
Wants=postgresql.service
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/db-backup.sh
StandardOutput=journal
StandardError=journal
Step 2: Create the timer file
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run Database Backup daily at 2:30 AM
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
Step 3: Enable and test
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer
systemctl list-timers db-backup.timer
# Test: run the service immediately
sudo systemctl start db-backup.service
# Check logs
journalctl -u db-backup.service -n 50
Step 4: Remove the cron entry
# Remove or comment out the line from /etc/cron.d/db-backup
sudo rm /etc/cron.d/db-backup
FAQ
Q: Can I use systemd timers as a regular user (without sudo)?
Yes. Create user units in ~/.config/systemd/user/ and use systemctl --user:
mkdir -p ~/.config/systemd/user
# Create ~/.config/systemd/user/mytask.service and mytask.timer
systemctl --user daemon-reload
systemctl --user enable --now mytask.timer
systemctl --user list-timers
User units run only while the user is logged in, unless you enable lingering:
sudo loginctl enable-linger username
Q: How do I run a timer at irregular intervals, like "5 minutes after the last run completes"?
Use OnUnitActiveSec in combination with OnBootSec:
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
This runs the service 1 minute after boot, then 5 minutes after each completion.
Q: My timer isn't running. How do I debug it?
# Check timer status
systemctl status mytask.timer
# Check when it last ran and when next
systemctl list-timers mytask.timer
# Check service logs
journalctl -u mytask.service -n 50
# Check for errors
journalctl -u mytask.timer -n 20
Common causes: daemon-reload was not run after creating the files, the timer was enabled but not started, or WantedBy=timers.target is missing from the [Install] section.