Scheduled Tasks (Timers)
systemd timers are a modern alternative to cron, offering more precise scheduling control, seamless log integration, and dependency management capabilities. On CentOS/AlmaLinux/Rocky Linux, an increasing number of system tasks have been migrated from cron to systemd timers.
Timers vs. Cron
Section titled “Timers vs. Cron”| Feature | systemd Timer | Cron |
|---|---|---|
| Log integration | Automatically integrates with journald; viewable via journalctl | Requires manual log redirection |
| Dependency management | Supports After=, Requires=, and other dependency declarations | No dependency management |
| Missed executions | Supports Persistent=true; missed tasks run at next startup | Missed tasks are lost |
| Resource control | Can set CPU, memory, and IO limits | No resource control |
| Precision | Supports microsecond-level precision | Minimum granularity is 1 minute |
| Boot delay | Supports OnBootSec= and other relative time triggers | Not supported |
| Random delay | Supports RandomizedDelaySec= to spread load | Not supported |
| Management | Unified management via systemctl | Requires editing crontab files |
How Timers Work
Section titled “How Timers Work”systemd timers work in pairs: each .timer unit corresponds to a .service unit with the same name. When the timer triggers, systemd automatically starts the corresponding service unit.
For example:
backup.timer— Defines the trigger schedulebackup.service— Defines the task to execute
Two Types of Timers
Section titled “Two Types of Timers”Monotonic Timers
Section titled “Monotonic Timers”Triggered based on relative time, suitable for “how long since a certain event” scenarios:
| Directive | Description |
|---|---|
OnBootSec= | Trigger after a specified time since system boot |
OnActiveSec= | Trigger after a specified time since the timer was activated |
OnStartupSec= | Trigger after a specified time since systemd started |
OnUnitActiveSec= | Trigger after a specified time since the corresponding service was last activated |
OnUnitInactiveSec= | Trigger after a specified time since the corresponding service last completed |
Calendar Timers (Realtime)
Section titled “Calendar Timers (Realtime)”Triggered based on absolute time, similar to cron scheduling:
| Directive | Description |
|---|---|
OnCalendar= | Trigger according to a calendar time expression |
OnCalendar Time Syntax
Section titled “OnCalendar Time Syntax”OnCalendar= uses the following format:
DayOfWeek Year-Month-Day Hour:Minute:SecondCommon Time Expressions
Section titled “Common Time Expressions”| Expression | Description | Cron Equivalent |
|---|---|---|
*-*-* 00:00:00 | Every day at midnight | 0 0 * * * |
*-*-* *:00:00 | Every hour on the hour | 0 * * * * |
*-*-* *:*:00 | Every minute | * * * * * |
*-*-01 00:00:00 | 1st of every month at midnight | 0 0 1 * * |
Mon *-*-* 08:00:00 | Every Monday at 8:00 AM | 0 8 * * 1 |
Mon..Fri *-*-* 18:00:00 | Weekdays at 6:00 PM | 0 18 * * 1-5 |
*-*-* 06,18:00:00 | Every day at 6:00 AM and 6:00 PM | 0 6,18 * * * |
*-*-* 09:00:00/2h | Every 2 hours starting at 9:00 AM | — |
systemd also provides convenient presets:
| Preset | Equivalent Expression |
|---|---|
minutely | *-*-* *:*:00 |
hourly | *-*-* *:00:00 |
daily | *-*-* 00:00:00 |
weekly | Mon *-*-* 00:00:00 |
monthly | *-*-01 00:00:00 |
yearly | *-01-01 00:00:00 |
Verifying Time Expressions
Section titled “Verifying Time Expressions”Use systemd-analyze calendar to verify whether an expression is correct and preview upcoming trigger times:
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"systemd-analyze calendar "*-*-* *:00/15:00"Creating a Scheduled Task: Complete Example
Section titled “Creating a Scheduled Task: Complete Example”The following example demonstrates how to create a timer + service pair using a database backup task.
Step 1: Create the Backup Script
Section titled “Step 1: Create the Backup Script”sudo vim /opt/scripts/db-backup.shEnter the following content:
#!/bin/bashBACKUP_DIR="/backup/database"TIMESTAMP=$(date +%Y%m%d_%H%M%S)mkdir -p "$BACKUP_DIR"mysqldump --all-databases | gzip > "$BACKUP_DIR/all-db-$TIMESTAMP.sql.gz"# Delete backups older than 7 daysfind "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 -deleteecho "Database backup completed: all-db-$TIMESTAMP.sql.gz"sudo chmod +x /opt/scripts/db-backup.shStep 2: Create the Service Unit File
Section titled “Step 2: Create the Service Unit File”sudo vim /etc/systemd/system/db-backup.service[Unit]Description=Database Backup ServiceAfter=mariadb.serviceWants=mariadb.service
[Service]Type=oneshotExecStart=/opt/scripts/db-backup.shUser=rootStandardOutput=journalStandardError=journal
# Timeout setting (large database backups may take a long time)TimeoutStartSec=3600
# Security hardeningPrivateTmp=trueProtectHome=trueNote: Services triggered by timers typically do not need an [Install] section, since they are started by the timer rather than directly enabled.
Step 3: Create the Timer Unit File
Section titled “Step 3: Create the Timer Unit File”sudo vim /etc/systemd/system/db-backup.timer[Unit]Description=Run Database Backup Daily at 2:00 AM
[Timer]OnCalendar=*-*-* 02:00:00RandomizedDelaySec=300Persistent=true
[Install]WantedBy=timers.targetConfiguration details:
| Directive | Description |
|---|---|
OnCalendar=*-*-* 02:00:00 | Trigger every day at 2:00 AM |
RandomizedDelaySec=300 | Random delay of 0-300 seconds, to avoid multiple servers executing simultaneously |
Persistent=true | If the execution time was missed (e.g., server was off), run immediately at next startup |
Step 4: Enable the Timer
Section titled “Step 4: Enable the Timer”sudo systemctl daemon-reloadsudo systemctl enable --now db-backup.timerStep 5: Verify the Timer
Section titled “Step 5: Verify the Timer”sudo systemctl status db-backup.timersudo systemctl start db-backup.servicesudo journalctl -u db-backup.service -n 20Using Monotonic Timers
Section titled “Using Monotonic Timers”In addition to calendar-based timers, monotonic timers are suitable for “execute at regular intervals” scenarios:
[Unit]Description=Run Health Check Every 5 Minutes
[Timer]OnBootSec=1minOnUnitActiveSec=5min
[Install]WantedBy=timers.target[Unit]Description=System Health Check
[Service]Type=oneshotExecStart=/opt/scripts/health-check.shListing and Managing Timers
Section titled “Listing and Managing Timers”View All Active Timers
Section titled “View All Active Timers”systemctl list-timersView All Timers (Including Inactive)
Section titled “View All Timers (Including Inactive)”systemctl list-timers --allTemporarily Stop a Timer
Section titled “Temporarily Stop a Timer”sudo systemctl stop db-backup.timerPermanently Disable a Timer
Section titled “Permanently Disable a Timer”sudo systemctl disable --now db-backup.timerMigrating from Cron to Timers
Section titled “Migrating from Cron to Timers”If you have an existing crontab task:
30 3 * * * /opt/scripts/cleanup.shThe corresponding systemd timer configuration is:
[Unit]Description=Daily Cleanup Task
[Service]Type=oneshotExecStart=/opt/scripts/cleanup.sh[Unit]Description=Run Cleanup Daily at 3:30 AM
[Timer]OnCalendar=*-*-* 03:30:00Persistent=true
[Install]WantedBy=timers.targetsudo systemctl daemon-reload && sudo systemctl enable --now cleanup.timerAfter confirming the timer is working correctly, remove the corresponding crontab entry:
sudo crontab -eTroubleshooting Timer Issues
Section titled “Troubleshooting Timer Issues”sudo systemctl status db-backup.timersudo systemctl status db-backup.servicesystemctl show db-backup.timersystemd-analyze calendar "*-*-* 02:00:00"sudo journalctl -u db-backup.timer -u db-backup.service --since today