Skip to content

Systemd Timers vs Cron: Modern Linux Task Automation

For decades, cron has been the default standard for scheduling background jobs in Unix-like operating systems. However, modern Linux systems powered by systemd offer a far superior, enterprise-grade alternative: Systemd Timers.


Why Migrate from Cron to Systemd Timers?

Featurecronsystemd Timers
LoggingSilent or requires custom file redirection (>> /var/log/...)Centralized in journalctl with stdout/stderr capture
Execution ControlsRuns blindly regardless of network/disk stateCan depend on targets (e.g., network-online.target)
Resource LimitsUnrestricted by defaultFull cgroups support (RAM, CPU limits)
Missed ExecutionsSkipped entirely if system is powered offA persistent calendar timer runs once after boot if at least one scheduled time was missed
Execution VisibilityDifficult to see next execution timeClean inspection with systemctl list-timers

Architecture: The Service & Timer Pair

A systemd timer requires two paired configuration files located in /etc/systemd/system/ (for system-wide tasks) or ~/.config/systemd/user/ (for unprivileged user tasks):

  1. .service file: Defines what executable or script to run.
  2. .timer file: Defines when and how often the paired service is triggered.

Step 1: Create the Working Script

01

Create the Working Script

Scripting

Create a configuration-backup script under /usr/local/bin/homelab-backup.sh. It skips example paths that are absent and fails clearly if none exist. This archives configuration only; use an application-aware backup method for databases and other live state.

Terminal window
sudo nano /usr/local/bin/homelab-backup.sh
❯ View Expected Console Output
#!/usr/bin/env bash
set -euo pipefail
umask 077
BACKUP_DIR="/var/backups/homelab"
DATE="$(date +'%Y-%m-%d_%H%M%S')"
mkdir -p "$BACKUP_DIR"
EXPECTED_PATHS=(etc/caddy etc/docker)
SOURCES=()
for path in "${EXPECTED_PATHS[@]}"; do
[[ -e "/$path" ]] && SOURCES+=("$path")
done
if ((${#SOURCES[@]} == 0)); then
echo "No configured paths exist; update EXPECTED_PATHS." >&2
exit 1
fi
echo "[$(date)] Starting configuration backup..."
tar -czf "$BACKUP_DIR/etc-backup-$DATE.tar.gz" -C / -- "${SOURCES[@]}"
# Prune archives older than 14 days.
find "$BACKUP_DIR" -type f -name "*.tar.gz" -mtime +14 -delete
echo "[$(date)] Backup completed successfully."

Ensure the script has executable permissions:

Terminal window
sudo chmod +x /usr/local/bin/homelab-backup.sh

Step 2: Define the Systemd Service Unit

02

Define the Systemd Service Unit

Systemd Unit

Create /etc/systemd/system/homelab-backup.service. We use Type=oneshot because batch backup tasks run to completion and exit immediately. We also enforce memory and CPU limits:

[Unit]
Description=Automated Homelab Backup Routine
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/homelab-backup.sh
User=root
StandardOutput=journal
StandardError=journal
MemoryMax=512M
CPUQuota=50%

Step 3: Define the Systemd Timer Unit

03

Define the Systemd Timer Unit

Scheduling

Create /etc/systemd/system/homelab-backup.timer. For this calendar timer, Persistent=true triggers one catch-up run after boot if at least one scheduled activation was missed while the system was off. It does not replay every missed interval.

[Unit]
Description=Trigger Homelab Backup Routine Daily
[Timer]
# Run every night at 03:30 AM
OnCalendar=*-*-* 03:30:00
# Randomized delay prevents I/O contention if multiple jobs fire together
RandomizedDelaySec=15m
# Catch up immediately if machine was asleep/offline
Persistent=true
[Install]
WantedBy=timers.target

Step 4: Understanding OnCalendar Schedule Expressions

04

Verify OnCalendar Schedule Expressions

Validation

Use the built-in systemd-analyze calendar utility to validate your schedule expressions and check when the next execution windows occur:

Terminal window
systemd-analyze calendar "Mon *-*-* 04:00:00"
❯ View Expected Console Output

Normalized form: Mon --* 04:00:00
Next elapse: the next Monday at 04:00 in the host’s local time zone
(in UTC): the equivalent UTC time
From now: varies with the time and date when the command is run

Quick Reference: Cron vs Systemd Timers

Intervalcron Equivalentsystemd Timer OnCalendar Expression
Every hour0 * * * *hourly or *-*-* *:00:00
Daily at midnight0 0 * * *daily or *-*-* 00:00:00
Every 15 minutes*/15 * * * **:0/15
Every Monday at 4 AM0 4 * * 1Mon *-*-* 04:00:00
Monthly on 1st at 2 AM0 2 1 * **-*-01 02:00:00

Step 5: Enable, Start & Verify the Timer

05

Enable, Start & Verify the Timer

Activation

Reload the systemd daemon to pick up the new unit files, then enable and start the timer (not the service directly):

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now homelab-backup.timer
systemctl list-timers --all homelab-backup.timer
❯ View Expected Console Output

NEXT LEFT LAST PASSED UNIT ACTIVATES
The next scheduled activation is calculated from the timer’s calendar and randomized delay; LAST is n/a before its first run.


Step 6: Testing Execution & Journal Auditing

06

Testing Execution & Journal Auditing

Monitoring

Trigger an on-demand manual execution of the service and inspect the real-time journal logs:

Terminal window
sudo systemctl start homelab-backup.service
sudo journalctl -u homelab-backup.service -n 50 --no-pager
❯ View Expected Console Output

— Journal begins at Fri 2026-10-02 00:00:00 CEST, ends at Fri 2026-10-02 13:00:00 CEST. —
Oct 02 13:05:00 server systemd[1]: Starting Automated Homelab Backup Routine…
Oct 02 13:05:01 server homelab-backup.sh[12450]: [Fri Oct 2 13:05:01 CEST 2026] Starting backup routine…
Oct 02 13:05:03 server homelab-backup.sh[12450]: [Fri Oct 2 13:05:03 CEST 2026] Backup completed successfully.
Oct 02 13:05:03 server systemd[1]: homelab-backup.service: Deactivated successfully.
Oct 02 13:05:03 server systemd[1]: Finished Automated Homelab Backup Routine.

Comments