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:

  1. A .service unit — defines what to run (the actual command or script)
  2. A .timer unit — 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:

DirectivePurpose
OnCalendarCalendar-based schedule (like cron)
Persistent=trueRun immediately if a scheduled run was missed (e.g., system was off)
WantedBy=timers.targetRequired 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:

ScheduleOnCalendar Expression
Every day at midnight*-*-* 00:00:00
Every Monday at 9 AMMon *-*-* 09:00:00
First of every month*-*-01 00:00:00
Every 15 minutes*-*-* *:00/15:00
Weekdays at 6 PMMon..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
DirectiveTriggers
OnBootSecX time after boot
OnStartupSecX time after systemd starts
OnUnitActiveSecX time after the timer last activated
OnUnitInactiveSecX 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:

CronOnCalendar
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
@rebootUse 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)
  • OnCalendar handles clock-based schedules; OnBootSec and OnUnitActiveSec handle interval-based timing
  • Persistent=true catches up on missed runs after downtime
  • systemd-analyze calendar lets you validate schedule expressions before deploying
  • Built-in logging via journalctl eliminates 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-run for 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