Systemd timers do everything cron does, plus some things cron cannot: they log to the journal, they can catch up on missed runs after a reboot, they support randomized delays to avoid thundering herds, and you can test them with systemctl start without waiting for the schedule to trigger. If your server runs systemd (which is every major distro since 2015), you already have timers available.
This guide covers creating systemd timers from scratch on Ubuntu 24.04, with practical examples you can adapt: disk monitoring, automated backups, log cleanup, user-level timers, and transient one-off timers. Every command and output below was tested on a real VM. For background on managing systemd services with systemctl, see our reference guide.
Tested March 2026 on Ubuntu 24.04 LTS with systemd 255
How Systemd Timers Work
A systemd timer consists of two unit files that work together:
- A
.timerunit that defines the schedule (when to run) - A
.serviceunit that defines the task (what to run)
The timer activates the service when the schedule triggers. By default, a timer named backup.timer activates backup.service (matching name, different suffix). Both files go in /etc/systemd/system/ for system-wide timers.
There are two types of timers:
| Type | Trigger | Example |
|---|---|---|
| Monotonic | Relative to an event (boot, last run) | OnBootSec=5min, OnUnitActiveSec=1h |
| Calendar | At specific dates/times | OnCalendar=*-*-* 02:00:00 (daily at 2am) |
Monotonic timers run relative to when the system booted or when the service last ran. Calendar timers run at specific wall-clock times, like cron.
Example 1: Disk Usage Monitor (Every 5 Minutes)
This timer checks root partition usage every 5 minutes and logs a warning when it exceeds 80%. It demonstrates a monotonic timer with OnUnitActiveSec.
Create the script that does the actual work:
sudo tee /usr/local/bin/disk-monitor.sh > /dev/null << 'EOF'
#!/bin/bash
USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "$TIMESTAMP - Root partition usage: ${USAGE}%" >> /var/log/disk-monitor.log
if [ "$USAGE" -gt 80 ]; then
echo "$TIMESTAMP - WARNING: Disk usage above 80%!" >> /var/log/disk-monitor.log
fi
EOF
sudo chmod +x /usr/local/bin/disk-monitor.sh
Create the service unit that runs the script:
sudo tee /etc/systemd/system/disk-monitor.service > /dev/null << 'EOF'
[Unit]
Description=Monitor disk usage and log alerts
[Service]
Type=oneshot
ExecStart=/usr/local/bin/disk-monitor.sh
EOF
The Type=oneshot tells systemd the service runs once and exits (not a long-running daemon). This is the correct type for timer-triggered tasks.
Create the timer unit:
sudo tee /etc/systemd/system/disk-monitor.timer > /dev/null << 'EOF'
[Unit]
Description=Run disk monitor every 5 minutes
[Timer]
OnBootSec=60
OnUnitActiveSec=5min
AccuracySec=1s
[Install]
WantedBy=timers.target
EOF
OnBootSec=60 runs the first check 60 seconds after boot. OnUnitActiveSec=5min repeats every 5 minutes after each run. AccuracySec=1s ensures the timer fires within 1 second of the scheduled time (the default is 1 minute, which batches timers for power efficiency).
Reload systemd, enable, and start the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now disk-monitor.timer
Verify the timer is active and scheduled:
systemctl status disk-monitor.timer
The output shows when the next trigger will fire:
● disk-monitor.timer - Run disk monitor every 5 minutes
Loaded: loaded (/etc/systemd/system/disk-monitor.timer; enabled; preset: enabled)
Active: active (waiting) since Wed 2026-03-25 11:02:37 UTC; 9ms ago
Trigger: Wed 2026-03-25 11:07:45 UTC; 4min left
Triggers: ● disk-monitor.service
Test the service manually without waiting for the timer:
sudo systemctl start disk-monitor.service
cat /var/log/disk-monitor.log
The log confirms it ran:
2026-03-25 11:02:37 - Root partition usage: 11%
Example 2: Daily Backup at 2 AM (Calendar Timer)
This timer runs a backup script every day at 2:00 AM. It uses OnCalendar for wall-clock scheduling, Persistent=true to catch up if the server was off at 2 AM, and RandomizedDelaySec to avoid all servers hitting the backup target at exactly the same second.
Create the backup script:
sudo tee /usr/local/bin/daily-backup.sh > /dev/null << 'EOF'
#!/bin/bash
BACKUP_DIR="/var/backups/app"
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
mkdir -p "$BACKUP_DIR"
tar czf "$BACKUP_DIR/etc-backup-${TIMESTAMP}.tar.gz" /etc/hostname /etc/hosts 2>/dev/null
echo "$(date '+%Y-%m-%d %H:%M:%S') - Backup completed: etc-backup-${TIMESTAMP}.tar.gz" >> /var/log/backup.log
# Keep only last 7 backups
ls -t "$BACKUP_DIR"/etc-backup-*.tar.gz 2>/dev/null | tail -n +8 | xargs rm -f 2>/dev/null
EOF
sudo chmod +x /usr/local/bin/daily-backup.sh
Create the service unit:
sudo tee /etc/systemd/system/daily-backup.service > /dev/null << 'EOF'
[Unit]
Description=Daily backup of critical configs
[Service]
Type=oneshot
ExecStart=/usr/local/bin/daily-backup.sh
EOF
Create the timer with calendar scheduling:
sudo tee /etc/systemd/system/daily-backup.timer > /dev/null << 'EOF'
[Unit]
Description=Run daily backup at 2:00 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
EOF
Persistent=true is the key difference from cron. If the server was powered off at 2 AM, systemd will run the backup immediately after the next boot. Cron would just skip it. RandomizedDelaySec=300 adds a random delay of up to 5 minutes, which prevents all servers in a fleet from starting their backups at the exact same second.
Enable and verify:
sudo systemctl daemon-reload
sudo systemctl enable --now daily-backup.timer
systemctl status daily-backup.timer
The timer shows the next trigger time with the randomized delay applied:
● daily-backup.timer - Run daily backup at 2:00 AM
Loaded: loaded (/etc/systemd/system/daily-backup.timer; enabled; preset: enabled)
Active: active (waiting) since Wed 2026-03-25 11:02:53 UTC
Trigger: Thu 2026-03-26 02:02:27 UTC; 14h left
Triggers: ● daily-backup.service
Test it manually:
sudo systemctl start daily-backup.service
cat /var/log/backup.log
2026-03-25 11:02:53 - Backup completed: etc-backup-20260325_110253.tar.gz
Example 3: Weekly Log Cleanup (Low Priority)
Maintenance tasks like log cleanup should run at low priority so they do not compete with production workloads. This timer demonstrates Nice=19 and IOSchedulingClass=idle to minimize system impact.
Create the cleanup script:
sudo tee /usr/local/bin/log-cleanup.sh > /dev/null << 'EOF'
#!/bin/bash
# Remove compressed logs older than 30 days
find /var/log -name "*.log.gz" -mtime +30 -delete 2>/dev/null
find /var/log -name "*.log.[0-9]" -mtime +30 -delete 2>/dev/null
# Truncate logs larger than 100MB
find /var/log -name "*.log" -size +100M -exec truncate -s 0 {} \; 2>/dev/null
# Clean systemd journal older than 7 days
journalctl --vacuum-time=7d --quiet 2>/dev/null
echo "$(date '+%Y-%m-%d %H:%M:%S') - Log cleanup completed" >> /var/log/cleanup.log
EOF
sudo chmod +x /usr/local/bin/log-cleanup.sh
Create the service with low CPU and I/O priority:
sudo tee /etc/systemd/system/log-cleanup.service > /dev/null << 'EOF'
[Unit]
Description=Clean old log files
[Service]
Type=oneshot
ExecStart=/usr/local/bin/log-cleanup.sh
Nice=19
IOSchedulingClass=idle
EOF
Nice=19 gives the process the lowest CPU priority. IOSchedulingClass=idle means it only does disk I/O when nothing else needs the disk. These settings are not available with cron.
Create the timer to run every Sunday at 3 AM:
sudo tee /etc/systemd/system/log-cleanup.timer > /dev/null << 'EOF'
[Unit]
Description=Weekly log cleanup
[Timer]
OnCalendar=Sun *-*-* 03:00:00
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
EOF
Enable and test:
sudo systemctl daemon-reload
sudo systemctl enable --now log-cleanup.timer
sudo systemctl start log-cleanup.service
systemctl status log-cleanup.service
The service exits cleanly with status=0/SUCCESS:
○ log-cleanup.service - Clean old log files
Loaded: loaded (/etc/systemd/system/log-cleanup.service; static)
Active: inactive (dead) since Wed 2026-03-25 11:03:57 UTC
TriggeredBy: ● log-cleanup.timer
Process: 1934 ExecStart=/usr/local/bin/log-cleanup.sh (code=exited, status=0/SUCCESS)
Main PID: 1934 (code=exited, status=0/SUCCESS)
CPU: 12ms
Example 4: Transient Timer (No Files Needed)
Sometimes you need a quick one-off scheduled task without creating permanent unit files. systemd-run creates transient timers on the fly:
sudo systemd-run --on-active=30s --unit=quick-task /usr/local/bin/disk-monitor.sh
This runs disk-monitor.sh once, 30 seconds from now. The timer and service are created automatically and cleaned up after execution. You can verify it was scheduled:
systemctl list-timers quick-task*
Other useful systemd-run patterns:
# Run a command at a specific time today
sudo systemd-run --on-calendar="2026-03-25 15:00:00" /usr/local/bin/daily-backup.sh
# Run a command every 10 minutes (persists until reboot)
sudo systemd-run --on-active=10min --on-unit-active=10min /usr/local/bin/disk-monitor.sh
# Run with a description for easier identification
sudo systemd-run --on-active=5min --description="One-off DB export" pg_dump mydb > /tmp/export.sql
Example 5: User Timer (No Root Required)
User timers run under a regular user account without sudo. The unit files go in ~/.config/systemd/user/ instead of /etc/systemd/system/. This is useful for personal automation like syncing files or checking for updates.
Create the user service directory and files:
mkdir -p ~/.config/systemd/user/
tee ~/.config/systemd/user/user-task.service > /dev/null << 'EOF'
[Unit]
Description=User-level task example
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo "User timer ran at $(date)" >> %h/user-timer.log'
EOF
tee ~/.config/systemd/user/user-task.timer > /dev/null << 'EOF'
[Unit]
Description=Run user task every 2 minutes
[Timer]
OnBootSec=30
OnUnitActiveSec=2min
[Install]
WantedBy=timers.target
EOF
The %h specifier expands to the user's home directory. Enable and start with the --user flag:
systemctl --user daemon-reload
systemctl --user enable --now user-task.timer
systemctl --user status user-task.timer
The timer runs under your user account:
● user-task.timer - Run user task every 2 minutes
Loaded: loaded (/home/ubuntu/.config/systemd/user/user-task.timer; enabled; preset: enabled)
Active: active (running) since Wed 2026-03-25 11:03:10 UTC
Trigger: n/a
Triggers: ● user-task.service
For user timers to run when you are not logged in, enable lingering for your account:
sudo loginctl enable-linger $USER
OnCalendar Syntax Reference
The OnCalendar directive uses the format DayOfWeek Year-Month-Day Hour:Minute:Second. All fields are optional except at least one time component. The systemd.time documentation has the full specification.
| Expression | When It Runs |
|---|---|
*-*-* *:00/15:00 | Every 15 minutes |
hourly | Every hour at :00 |
daily | Every day at midnight |
*-*-* 02:00:00 | Every day at 2:00 AM |
weekly | Every Monday at midnight |
Mon *-*-* 09:00:00 | Every Monday at 9:00 AM |
Mon..Fri *-*-* 18:00:00 | Weekdays at 6:00 PM |
*-*-01 03:00:00 | First of every month at 3:00 AM |
*-01,04,07,10-01 00:00:00 | Quarterly (Jan, Apr, Jul, Oct 1st) |
Sun *-*-* 03:00:00 | Every Sunday at 3:00 AM |
monthly | First of every month at midnight |
yearly | January 1st at midnight |
Validate any expression before deploying with systemd-analyze calendar:
systemd-analyze calendar "Mon..Fri *-*-* 18:00:00"
The output shows the normalized form and the next trigger time:
Normalized form: Mon..Fri *-*-* 18:00:00
Next elapse: Wed 2026-03-25 18:00:00 UTC
From now: 6h left
This catches syntax errors before they cause a timer to never fire. Always test expressions with systemd-analyze calendar first.
Timer Configuration Reference
Key directives for the [Timer] section:
| Directive | Purpose | Example |
|---|---|---|
OnBootSec | Run N seconds/minutes after boot | OnBootSec=5min |
OnUnitActiveSec | Repeat N after each run | OnUnitActiveSec=1h |
OnCalendar | Wall-clock schedule | OnCalendar=daily |
Persistent | Catch up missed runs after downtime | Persistent=true |
RandomizedDelaySec | Add random delay to avoid thundering herd | RandomizedDelaySec=5min |
AccuracySec | Timer precision (default 1min) | AccuracySec=1s |
OnStartupSec | Run N after systemd starts (user timers) | OnStartupSec=30 |
OnUnitInactiveSec | Run N after service becomes inactive | OnUnitInactiveSec=30min |
Unit | Override which service to trigger | Unit=other.service |
List and Monitor Timers
View all active timers on the system:
systemctl list-timers
The output shows the next trigger time, time until trigger, last run, and which service each timer activates:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Wed 2026-03-25 11:07:45 UTC 3min 48s Wed 2026-03-25 11:02:45 UTC 1min ago disk-monitor.timer disk-monitor.service
Thu 2026-03-26 02:02:01 UTC 14h - - daily-backup.timer daily-backup.service
Sun 2026-03-29 03:08:26 UTC 3 days - - log-cleanup.timer log-cleanup.service
Include inactive timers with --all:
systemctl list-timers --all
Check the journal for timer-triggered service runs:
journalctl -u disk-monitor.service --no-pager -n 10
The journal shows exact start/finish times and exit status for every run:
Mar 25 11:02:45 ubuntu systemd[1]: Starting disk-monitor.service - Monitor disk usage and log alerts...
Mar 25 11:02:45 ubuntu systemd[1]: disk-monitor.service: Deactivated successfully.
Mar 25 11:02:45 ubuntu systemd[1]: Finished disk-monitor.service - Monitor disk usage and log alerts.
Stop and Disable Timers
Stop a running timer (it will not fire again until manually started or re-enabled after reboot):
sudo systemctl stop disk-monitor.timer
Disable it so it does not start on boot:
sudo systemctl disable disk-monitor.timer
Remove the timer and service files entirely:
sudo systemctl stop disk-monitor.timer
sudo systemctl disable disk-monitor.timer
sudo rm /etc/systemd/system/disk-monitor.timer /etc/systemd/system/disk-monitor.service
sudo systemctl daemon-reload
Systemd Timers vs Cron
| Feature | Systemd Timers | Cron |
|---|---|---|
| Catch up missed runs | Yes (Persistent=true) | No (missed = skipped) |
| Logging | Journald (structured, queryable) | Email or syslog |
| Test without waiting | systemctl start service | Not possible |
| Random delay | RandomizedDelaySec | Manual sleep $RANDOM |
| CPU/IO priority | Nice=, IOSchedulingClass= | Must set in script |
| Resource limits | cgroups (MemoryMax, CPUQuota) | None built-in |
| Dependencies | After=, Requires= | None |
| Configuration | Two files (.timer + .service) | One line in crontab |
| Second precision | Yes | Minute only |
| User-level tasks | systemctl --user | crontab -e |
Cron is still the faster option for simple, single-line scheduled commands. When you need logging, missed-run recovery, resource controls, or dependency ordering, systemd timers with persistent journal storage are the better choice.