systemd timers are the modern replacement for cron on Linux systems. They give you everything cron does — recurring and one-shot scheduling — plus built-in logging, dependency management, resource controls, and the ability to catch up on missed runs. If you manage Linux servers, switching from cron to systemd timers means fewer silent failures and far better visibility into what your scheduled tasks are doing.
This guide walks you through creating timers from scratch, converting existing cron jobs, and using advanced features like randomized delays and persistent scheduling.
Prerequisites
- A Linux system running systemd (Ubuntu 16.04+, CentOS 7+, Debian 8+, or any modern distribution)
- Root or sudo access
- Basic familiarity with systemctl and unit files
How systemd Timers Work
A systemd timer consists of two unit files:
- A
.serviceunit — defines what to run (the actual command or script) - A
.timerunit — defines when to run it (the schedule)
The timer unit activates its matching service unit on schedule. Both files must share the same base name (e.g., backup.service and backup.timer), unless you explicitly specify a different unit with Unit=.
This separation is a key advantage over cron: the service unit can be tested independently, restarted on failure, and inspected with standard systemd tools.
Creating Your First Timer
Let’s create a timer that runs a cleanup script every day at 2:00 AM.
Step 1: Create the Service Unit
sudo nano /etc/systemd/system/daily-cleanup.service
[Unit]
Description=Daily temporary file cleanup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup.sh
The Type=oneshot tells systemd this process runs to completion and exits — it’s not a long-running daemon. This is the correct type for scheduled tasks.
Create the script it references:
sudo tee /usr/local/bin/cleanup.sh > /dev/null << 'EOF'
#!/bin/bash
find /tmp -type f -mtime +7 -delete
find /var/log -name "*.gz" -mtime +30 -delete
echo "Cleanup completed at $(date)"
EOF
sudo chmod +x /usr/local/bin/cleanup.sh
Step 2: Create the Timer Unit
sudo nano /etc/systemd/system/daily-cleanup.timer
[Unit]
Description=Run daily cleanup at 2:00 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Key directives:
| Directive | Purpose |
|---|---|
OnCalendar | Calendar-based schedule (like cron) |
Persistent=true | Run immediately if a scheduled run was missed (e.g., system was off) |
WantedBy=timers.target | Required for systemctl enable to work |
Step 3: Enable and Start
sudo systemctl daemon-reload
sudo systemctl enable --now daily-cleanup.timer
Step 4: Verify
systemctl list-timers daily-cleanup.timer
Output:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Wed 2025-12-24 02:00:00 UTC 10h left n/a n/a daily-cleanup.timer daily-cleanup.service
Test the service manually to confirm it works:
sudo systemctl start daily-cleanup.service
journalctl -u daily-cleanup.service --no-pager -n 20
Understanding OnCalendar Syntax
The OnCalendar directive uses a flexible calendar expression format:
DayOfWeek Year-Month-Day Hour:Minute:Second
Here are the most common patterns:
| Schedule | OnCalendar Expression |
|---|---|
| Every day at midnight | *-*-* 00:00:00 |
| Every Monday at 9 AM | Mon *-*-* 09:00:00 |
| First of every month | *-*-01 00:00:00 |
| Every 15 minutes | *-*-* *:00/15:00 |
| Weekdays at 6 PM | Mon..Fri *-*-* 18:00:00 |
| Every Jan and Jul 1st | *-01,07-01 00:00:00 |
| Every 2 hours | *-*-* 00/2:00:00 |
You can validate expressions with systemd-analyze calendar:
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
Original form: Mon..Fri *-*-* 09:00:00
Normalized form: Mon..Fri *-*-* 09:00:00
Next elapse: Mon 2025-12-29 09:00:00 UTC
(in UTC): Mon 2025-12-29 09:00:00 UTC
From now: 5 days left
This is one of the biggest advantages over cron — you can verify your schedule expression before deploying it.
Monotonic Timers: Intervals After Events
Not all schedules are clock-based. Monotonic timers trigger relative to an event:
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
| Directive | Triggers |
|---|---|
OnBootSec | X time after boot |
OnStartupSec | X time after systemd starts |
OnUnitActiveSec | X time after the timer last activated |
OnUnitInactiveSec | X time after the service last finished |
This is useful for health checks, metric collection, or any task that should run at intervals regardless of wall-clock time.
Example — run a health check every 5 minutes, starting 1 minute after boot:
[Unit]
Description=Periodic health check
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
AccuracySec=10s
[Install]
WantedBy=timers.target
The AccuracySec=10s tightens the default 1-minute coalescing window, so your timer fires closer to exactly every 5 minutes.
Advanced Features
Randomized Delay
Prevent multiple servers from hitting the same resource simultaneously:
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=900
This schedules the task between 3:00 AM and 3:15 AM, with systemd picking a random offset for each boot cycle.
Resource Controls
Since the task runs as a systemd service, you can apply resource limits:
[Service]
Type=oneshot
ExecStart=/usr/local/bin/heavy-report.sh
CPUQuota=50%
MemoryMax=512M
IOWeight=50
Nice=15
Cron has no equivalent — you would need to add nice, ionice, and ulimit calls inside every script.
Failure Notification
Trigger an alert when a scheduled task fails:
[Unit]
Description=Database backup
OnFailure=notify-admin@%n.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/db-backup.sh
Create the notification template at /etc/systemd/system/notify-admin@.service:
[Unit]
Description=Send failure alert for %i
[Service]
Type=oneshot
ExecStart=/usr/local/bin/send-alert.sh "Unit %i failed on %H"
Converting Cron Jobs to systemd Timers
Here’s a systematic approach for migrating existing cron jobs.
Example cron entry:
30 2 * * 1-5 /usr/local/bin/db-backup.sh
This runs the backup at 2:30 AM on weekdays. The equivalent systemd units:
# /etc/systemd/system/db-backup.service
[Unit]
Description=Database backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/db-backup.sh
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Database backup on weekday nights
[Timer]
OnCalendar=Mon..Fri *-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
Cron-to-OnCalendar quick reference:
| Cron | OnCalendar |
|---|---|
0 * * * * (hourly) | *-*-* *:00:00 |
*/5 * * * * (every 5 min) | *-*-* *:00/5:00 |
0 0 * * 0 (weekly Sun) | Sun *-*-* 00:00:00 |
0 0 1 * * (monthly) | *-*-01 00:00:00 |
@reboot | Use OnBootSec=0 |
After creating the units:
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer
# Verify
systemctl list-timers db-backup.timer
# Comment out the old cron entry
sudo crontab -e
Monitoring and Debugging Timers
List All Active Timers
systemctl list-timers --all
Check Timer Logs
journalctl -u daily-cleanup.service --since today
journalctl -u daily-cleanup.service --since "2025-12-20" --until "2025-12-23"
Debug a Timer That Won’t Fire
# Check timer status
systemctl status daily-cleanup.timer
# Verify the calendar expression
systemd-analyze calendar "*-*-* 02:00:00"
# Check for unit file errors
systemd-analyze verify /etc/systemd/system/daily-cleanup.*
# View all timers including inactive
systemctl list-timers --all --no-pager
Transient Timers for One-Off Tasks
Need to run something once in 30 minutes without creating unit files?
systemd-run --on-active=30min /usr/local/bin/one-time-task.sh
Or at a specific time:
systemd-run --on-calendar="2025-12-24 15:00:00" /usr/local/bin/holiday-report.sh
Troubleshooting
Timer shows “n/a” for NEXT: The timer is loaded but not enabled. Run systemctl enable --now <name>.timer.
Service runs but timer doesn’t trigger it: Ensure the .timer and .service filenames match exactly. Check with systemctl cat <name>.timer for typos.
“Failed to parse calendar specification”: Your OnCalendar syntax is wrong. Validate with systemd-analyze calendar "your expression".
Timer fires late: The default AccuracySec is 1 minute. systemd coalesces wake-ups to save power. Set AccuracySec=1s if you need precision.
Missed runs not caught up: You forgot Persistent=true in the [Timer] section.
Summary
- systemd timers replace cron with two unit files: a
.service(what to run) and a.timer(when to run it) OnCalendarhandles clock-based schedules;OnBootSecandOnUnitActiveSechandle interval-based timingPersistent=truecatches up on missed runs after downtimesystemd-analyze calendarlets you validate schedule expressions before deploying- Built-in logging via
journalctleliminates the need for custom log redirection - Resource controls (
CPUQuota,MemoryMax) and failure notifications give you production-grade reliability - Every cron job can be migrated to a systemd timer with straightforward syntax mapping
- Use
systemd-runfor quick one-off scheduled tasks without creating unit files
Configure Cron Jobs on Linux | journalctl: Query and Analyze Linux System Logs | Managing Linux Services with systemctl