Systemd Timers: The Modern Cron Replacement — Complete Setup and Migration Guide
🎯 Key Takeaways
- Table of Contents
- Systemd Timers vs Cron: When to Use Each
- Anatomy of a Systemd Timer: The .timer and .service Pair
- Calendar Timers: Scheduled Time-Based Execution
- Monotonic Timers: Relative Time Execution
📑 Table of Contents
- Table of Contents
- Systemd Timers vs Cron: When to Use Each
- Anatomy of a Systemd Timer: The .timer and .service Pair
- Calendar Timers: Scheduled Time-Based Execution
- Monotonic Timers: Relative Time Execution
- Calendar Expression Syntax Reference
- Creating Your First Systemd Timer
- Managing and Monitoring Timers
- Transient Timers with systemd-run
- Migrating Cron Jobs to Systemd Timers
- Logging and Debugging Timer Execution
- Real-World Timer Examples
- Conclusion
Systemd timers are the modern replacement for cron jobs on Linux systems running systemd. While cron has served Linux administrators for decades, systemd timers offer significant advantages: built-in logging through journald, dependency management with other systemd units, monotonic timers that catch up after downtime, per-service resource limits, and no need to manage crontab files across multiple users. This guide covers creating systemd timer units, migrating from cron, handling calendar and monotonic schedules, and monitoring timer execution.
📑 Table of Contents
- Table of Contents
- Systemd Timers vs Cron: When to Use Each
- Anatomy of a Systemd Timer: The .timer and .service Pair
- Calendar Timers: Scheduled Time-Based Execution
- Monotonic Timers: Relative Time Execution
- Calendar Expression Syntax Reference
- Creating Your First Systemd Timer
- Managing and Monitoring Timers
- Transient Timers with systemd-run
- Migrating Cron Jobs to Systemd Timers
- Logging and Debugging Timer Execution
- Real-World Timer Examples
- Certbot SSL Certificate Renewal
- Database Backup
- Disk Space Monitor (Every 15 Minutes)
- Conclusion
Table of Contents
- Systemd Timers vs Cron: When to Use Each
- Anatomy of a Systemd Timer: The .timer and .service Pair
- Calendar Timers: Scheduled Time-Based Execution
- Monotonic Timers: Relative Time Execution
- Calendar Expression Syntax Reference
- Creating Your First Systemd Timer
- Managing and Monitoring Timers
- Transient Timers with systemd-run
- Migrating Cron Jobs to Systemd Timers
- Logging and Debugging Timer Execution
- Real-World Timer Examples
Systemd Timers vs Cron: When to Use Each
Both cron and systemd timers schedule recurring tasks, but they differ in important ways:
| Feature | Cron | Systemd Timer |
|---|---|---|
| Logging | Output captured only if redirected | Full journal logging (stdout/stderr) |
| Missed job handling | Skipped if system was off | Can catch up with Persistent=true |
| Resource limits | None | CPU, memory, I/O limits via cgroup |
| Dependencies | None | Can require/wait for services |
| Randomized delay | Not built-in | Built-in with RandomizedDelaySec |
| User scope | Per-user crontabs | System or per-user units |
| Config location | /etc/cron.d/, crontab |
/etc/systemd/system/ |
| Syntax | Compact 5-field format | Verbose but self-documenting |
Use systemd timers for: system tasks needing logging, services with dependencies, tasks where missed executions must be caught up, and tasks that should run as specific service accounts with resource limits.
Stick with cron for: per-user scheduling, quick one-liners, environments where systemd is not the init system, or teams more comfortable with cron syntax.
Anatomy of a Systemd Timer: The .timer and .service Pair
Every systemd timer consists of two unit files that share the same base name:
backup.timer— defines when to run (the schedule)backup.service— defines what to run (the command)
When the timer fires, it activates the matching .service unit. The service runs the command, logs all output to the journal, then exits. The timer waits for the next trigger.
# Timer unit structure
[Unit]
Description=Human-readable description of what this timer does
[Timer]
OnCalendar=*-*-* 02:00:00 # Schedule (calendar) or
OnBootSec=10min # Schedule (monotonic)
Persistent=true # Catch up missed runs after downtime
RandomizedDelaySec=5min # Add up to 5 minutes random delay (avoids stampede)
Unit=backup.service # Optional: explicit service name (defaults to same base name)
[Install]
WantedBy=timers.target # Always use timers.target
# Service unit structure (paired with the timer)
[Unit]
Description=Backup script executed by backup.timer
[Service]
Type=oneshot # Task runs and exits (vs long-running daemon)
User=backup # Run as this user
ExecStart=/usr/local/bin/backup.sh
# Optional resource limits:
MemoryMax=512M
CPUQuota=25%
Nice=10 # Lower scheduling priority
IOSchedulingClass=idle # Don't compete with interactive I/O
Calendar Timers: Scheduled Time-Based Execution
Calendar timers fire at specific wall-clock times, similar to cron. The OnCalendar= directive accepts a flexible time expression.
# Common calendar expressions
OnCalendar=daily # Midnight every day
OnCalendar=weekly # Monday midnight
OnCalendar=monthly # First of month at midnight
OnCalendar=hourly # Every hour at :00
OnCalendar=minutely # Every minute
# Specific time
OnCalendar=*-*-* 02:30:00 # Every day at 02:30
OnCalendar=Mon *-*-* 06:00:00 # Every Monday at 06:00
OnCalendar=Sat,Sun *-*-* 08:00:00 # Weekends at 08:00
# First day of every month
OnCalendar=*-*-01 03:00:00
# Every 15 minutes
OnCalendar=*:0/15
# Specific months: every January and July at midnight
OnCalendar=*-1,7-01 00:00:00
# Verify your expression before deploying
systemd-analyze calendar "Mon *-*-* 06:00:00"
# Output shows next trigger times
Monotonic Timers: Relative Time Execution
Monotonic timers fire relative to a system event (boot, service start, last run), not a wall-clock time.
# Run 5 minutes after boot
OnBootSec=5min
# Run 10 minutes after systemd started (slightly later than boot)
OnStartupSec=10min
# Run 1 hour after the unit was last activated
OnUnitActiveSec=1h
# Run 30 minutes after the last time the unit finished
OnUnitInactiveSec=30min
# Combine — run 2 minutes after boot, then every 6 hours
OnBootSec=2min
OnUnitActiveSec=6h
Calendar Expression Syntax Reference
# Full syntax: DayOfWeek Year-Month-Day Hour:Minute:Second
# Component wildcards:
# * = every value
# /N = every Nth value
# a,b = multiple values
# a..b = range
# Examples:
OnCalendar=*-*-* *:00:00 # Every hour
OnCalendar=*-*-* *:0/30:00 # Every 30 minutes
OnCalendar=*-*-1/7 00:00:00 # Every 7 days starting on the 1st
OnCalendar=Mon..Fri *-*-* 09:00:00 # Weekdays at 9 AM
OnCalendar=2026-*-* 00:00:00 # Every midnight in 2026
# Special aliases:
# hourly = *-*-* *:00:00
# daily = *-*-* 00:00:00
# weekly = Mon *-*-* 00:00:00
# monthly = *-*-01 00:00:00
# quarterly = *-1,4,7,10-01 00:00:00
# yearly = *-01-01 00:00:00
# Test any expression:
systemd-analyze calendar "Mon..Fri *-*-* 08:00:00"
# Next elapse: Thu 2026-05-07 08:00:00 UTC
# (in UTC: 0 day 12h 30min 22.435521s)
Creating Your First Systemd Timer
A practical example: a daily log cleanup job.
# Step 1: Write the script
cat > /usr/local/bin/log-cleanup.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
# Delete log files older than 30 days from /var/log/app/
find /var/log/app/ -name "*.log" -mtime +30 -delete
echo "Log cleanup complete: $(date)"
SCRIPT
chmod +x /usr/local/bin/log-cleanup.sh
# Step 2: Create the service unit
cat > /etc/systemd/system/log-cleanup.service << 'UNIT'
[Unit]
Description=Delete application log files older than 30 days
Documentation=man:find(1)
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/log-cleanup.sh
StandardOutput=journal
StandardError=journal
UNIT
# Step 3: Create the timer unit
cat > /etc/systemd/system/log-cleanup.timer << 'UNIT'
[Unit]
Description=Run log cleanup daily at 03:00
Documentation=man:systemd.timer(5)
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10min
[Install]
WantedBy=timers.target
UNIT
# Step 4: Reload and enable
systemctl daemon-reload
systemctl enable --now log-cleanup.timer
# Step 5: Verify
systemctl status log-cleanup.timer
systemctl list-timers log-cleanup.timer
Managing and Monitoring Timers
# List all timers with next/last execution times
systemctl list-timers
# List only active timers
systemctl list-timers --all
# Start a timer now (for testing)
systemctl start log-cleanup.timer
# Run the service immediately (bypass timer, for testing)
systemctl start log-cleanup.service
# Check timer status
systemctl status log-cleanup.timer
# Disable a timer (stops it from running at boot)
systemctl disable log-cleanup.timer
# Stop a currently running timer (but doesn't disable it)
systemctl stop log-cleanup.timer
# See the full unit file as loaded
systemctl cat log-cleanup.timer
Transient Timers with systemd-run
For one-off scheduled tasks, systemd-run creates a transient timer without needing to write a unit file.
# Run a command in 5 minutes
systemd-run --on-active=5min /usr/bin/certbot renew
# Run a command at a specific time
systemd-run --on-calendar="2026-05-01 08:00:00" /usr/local/bin/maintenance.sh
# Run a command every hour temporarily (for testing)
systemd-run --on-unit-active=1h --timer-property=Persistent=true \
/usr/local/bin/health-check.sh
# List transient timers
systemctl list-timers | grep transient
# The transient unit disappears after it fires (unless it has an active schedule)
Migrating Cron Jobs to Systemd Timers
# Common cron → systemd timer translations
# CRON: 0 2 * * * /usr/local/bin/backup.sh
# → OnCalendar=*-*-* 02:00:00
# CRON: */15 * * * * /usr/local/bin/check.sh
# → OnCalendar=*:0/15
# CRON: 0 0 1 * * /usr/local/bin/monthly-report.sh
# → OnCalendar=*-*-01 00:00:00
# CRON: 0 9-17 * * 1-5 /usr/local/bin/business-hours.sh
# → OnCalendar=Mon..Fri *-*-* 09,10,11,12,13,14,15,16,17:00:00
# or more cleanly, use AccuracySec and OnCalendar=Mon..Fri *-*-* 09:00:00
# CRON: @reboot /usr/local/bin/startup.sh
# → Use a service with [Install] WantedBy=multi-user.target
# Or: OnBootSec=10sec (with a timer)
# Migration script: read existing crontab and create timer stubs
crontab -l > /tmp/current-crontab.txt
# Then manually create .service and .timer files for each entry
Logging and Debugging Timer Execution
# View all output from the last timer run
journalctl -u log-cleanup.service
# Follow logs in real time while the service runs
journalctl -u log-cleanup.service -f
# View logs for the last 24 hours
journalctl -u log-cleanup.service --since "24 hours ago"
# View both timer and service logs together
journalctl -u log-cleanup.timer -u log-cleanup.service
# Check exit codes — see if the last run succeeded
systemctl show log-cleanup.service --property=ExecMainStatus
# 0 = success, non-zero = failure
# Enable persistent logging (so timer history survives reboots)
mkdir -p /var/log/journal
systemd-tmpfiles --create --prefix /var/log/journal
# Debug a failing service directly
systemd-analyze verify log-cleanup.service # Check unit file syntax
systemctl start log-cleanup.service # Run manually and watch for errors
journalctl -u log-cleanup.service -n 50 # Last 50 lines
Real-World Timer Examples
Certbot SSL Certificate Renewal
cat > /etc/systemd/system/certbot-renew.service << 'UNIT'
[Unit]
Description=Certbot Certificate Renewal
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
UNIT
cat > /etc/systemd/system/certbot-renew.timer << 'UNIT'
[Unit]
Description=Twice-daily certificate renewal check
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
UNIT
systemctl daemon-reload && systemctl enable --now certbot-renew.timer
Database Backup
cat > /etc/systemd/system/pg-backup.service << 'UNIT'
[Unit]
Description=PostgreSQL Daily Backup
After=postgresql.service
Requires=postgresql.service
[Service]
Type=oneshot
User=postgres
ExecStart=/bin/bash -c 'pg_dumpall | gzip > /backups/pg-$(date +%%Y%%m%%d).sql.gz'
Nice=10
IOSchedulingClass=idle
MemoryMax=512M
UNIT
cat > /etc/systemd/system/pg-backup.timer << 'UNIT'
[Unit]
Description=Run PostgreSQL backup daily at 01:00
[Timer]
OnCalendar=*-*-* 01:00:00
Persistent=true
RandomizedDelaySec=15min
[Install]
WantedBy=timers.target
UNIT
systemctl daemon-reload && systemctl enable --now pg-backup.timer
Disk Space Monitor (Every 15 Minutes)
cat > /etc/systemd/system/disk-monitor.service << 'UNIT'
[Unit]
Description=Check disk space and alert if over threshold
[Service]
Type=oneshot
ExecStart=/usr/local/bin/check-disk.sh
UNIT
cat > /etc/systemd/system/disk-monitor.timer << 'UNIT'
[Unit]
Description=Check disk space every 15 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
UNIT
Conclusion
Systemd timers bring scheduled task management into the same framework as service management — the same systemctl commands, the same journald logging, the same resource controls. The two-file structure is more verbose than a crontab line, but the payoff is immediate: every timer run is logged automatically, failures are visible with systemctl status, and Persistent=true ensures a missed 2 AM backup catches up when the server comes back online. For new deployments on any systemd-based Linux system, timers are the right default for scheduled tasks. Cron remains useful for per-user scheduling and quick operational tasks, but system-level automation belongs in /etc/systemd/system/.
Was this article helpful?
About Ramesh Sundararamaiah
Red Hat Certified Architect
Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.