Schedule Backups with systemd Timers
Use systemd timers to schedule reliable backups with OnCalendar, RandomizedDelaySec, and Persistent=true — replacing cron with better logging and catch-up runs.
Before you start
- ▸A working systemd-based Linux installation (systemd 232 or later for full Persistent= support)
- ▸rsync installed, or substitute your preferred backup tool in the script
- ▸A writable backup destination (local disk, NAS mount, or external drive) available at /mnt/backup
- ▸Root or sudo access to write files in /etc/systemd/system/
systemd timers are the modern replacement for cron jobs on most Linux distributions. They offer better logging (via journald), dependency handling, catch-up execution for missed jobs, and fine-grained control over scheduling. This guide walks through writing a real backup timer from scratch, explaining the key directives along the way, and shows you how to verify and troubleshoot it.
systemd Timers vs. cron
cron is not going away, but systemd timers have concrete advantages for scheduled system tasks:
- Logging: All output is captured by journald automatically. No more lost stdout or misconfigured MAILTO.
- Missed runs: With
Persistent=true, a timer that was skipped (system was off) runs at next boot. - Dependencies: You can require network interfaces, mounts, or other units to be active before the job fires.
- Randomized delay:
RandomizedDelaySecspreads load across a fleet without per-machine crontab edits. - Introspection:
systemctl list-timersshows every timer, last run, and next run in one place.
cron is still reasonable for per-user jobs or when you need the MAILTO pattern. For system-level maintenance like backups, timers are the better fit.
How a Timer Works
A systemd timer is always paired with a service unit of the same base name. When the timer fires, systemd activates the service. You write two files: backup.timer and backup.service. The timer handles when; the service handles what.
Step 1: Write the Backup Script
Put your backup logic in a standalone script so it is easy to test independently. This example uses rsync to back up /etc and /home to a local destination. Adapt the paths and tool to your environment.
sudo mkdir -p /usr/local/lib/backups
sudo nano /usr/local/lib/backups/system-backup.sh
Paste the following content:
#!/usr/bin/env bash
set -euo pipefail
DEST="/mnt/backup/$(hostname)"
DATE=$(date +%Y-%m-%d)
mkdir -p "${DEST}/${DATE}"
rsync -aAX --delete \
/etc \
/home \
"${DEST}/${DATE}/"
echo "Backup completed: ${DEST}/${DATE}"
sudo chmod +x /usr/local/lib/backups/system-backup.sh
Test the script manually before wiring up the timer:
sudo /usr/local/lib/backups/system-backup.sh
Step 2: Write the Service Unit
Create the service unit that the timer will activate. Place system-wide units in /etc/systemd/system/.
sudo nano /etc/systemd/system/system-backup.service
[Unit]
Description=System backup (rsync /etc and /home)
Requires=local-fs.target
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/local/lib/backups/system-backup.sh
User=root
IOSchedulingClass=best-effort
IOSchedulingPriority=7
Nice=19
Type=oneshot is correct for tasks that run to completion and exit. Nice=19 and the IO scheduling settings keep the backup from competing with interactive workloads. There is no [Install] section because the service is activated by the timer, not enabled directly.
Step 3: Write the Timer Unit
sudo nano /etc/systemd/system/system-backup.timer
[Unit]
Description=Daily system backup timer
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=20min
Persistent=true
[Install]
WantedBy=timers.target
Key Directives Explained
- OnCalendar=*-*-* 02:30:00 — Fires every day at 02:30 local time. The format is
Year-Month-Day Hour:Minute:Second. UseMon..Fri *-*-* 06:00:00for weekday-only, orweekly/monthlyas shorthand values. - RandomizedDelaySec=20min — Adds a random delay between 0 and 20 minutes after the scheduled time. Invaluable on multiple machines sharing a backup destination — without it, all nodes hammer the NAS simultaneously at 02:30.
- Persistent=true — If the system was powered off or suspended at 02:30, the timer fires the job shortly after the next boot. Without this, the run is simply skipped. This is the equivalent of
anacronbehavior and is one of the strongest arguments for timers over plain cron.
Validating Your OnCalendar Expression
Before enabling, confirm systemd parses your calendar expression as expected:
systemd-analyze calendar "*-*-* 02:30:00"
Output will show the normalized form, the next scheduled trigger, and the time until that trigger. Adjust the string until it matches your intent.
More OnCalendar Examples
| Expression | Meaning |
|---|---|
hourly | Every hour on the hour |
daily | Every day at 00:00 |
Mon *-*-* 04:00:00 | Every Monday at 04:00 |
*-*-1 03:00:00 | First of every month at 03:00 |
*-*-* 00/6:00:00 | Every 6 hours |
Step 4: Enable and Start the Timer
Reload the systemd daemon so it picks up the new unit files, then enable and start the timer. Enable registers it to start at boot; start activates it immediately in the current session.
sudo systemctl daemon-reload
sudo systemctl enable --now system-backup.timer
Note: enable the timer, not the service. The service has no [Install] section and should not be enabled directly.
Step 5: Verify the Timer Is Running
List all active timers and confirm yours appears with a sensible next-trigger time:
systemctl list-timers --all | grep backup
You will see columns for NEXT, LEFT, LAST, PASSED, UNIT, and ACTIVATES. Confirm the NEXT timestamp looks correct and ACTIVATES shows system-backup.service.
Trigger the service manually to confirm the end-to-end chain works without waiting for 02:30:
sudo systemctl start system-backup.service
systemctl status system-backup.service
Check the logs:
journalctl -u system-backup.service -n 50
Per-User Timers
You can run timers as a regular user without root, which is closer to a per-user crontab. Store unit files in ~/.config/systemd/user/ and use the --user flag:
systemctl --user daemon-reload
systemctl --user enable --now my-backup.timer
journalctl --user -u my-backup.service
For user timers to run when the user is logged out, loginctl enable-linger username must be set on the account.
Distro Package Notes
rsync is available on all major distributions but the package name varies slightly:
- Debian/Ubuntu:
sudo apt install rsync - Fedora/RHEL/Rocky:
sudo dnf install rsync - Arch:
sudo pacman -S rsync
The systemd unit file syntax is identical across distributions. SELinux on Fedora/RHEL may require a policy adjustment if your backup destination is on a non-standard mount; check ausearch -m avc if the service fails silently.
Troubleshooting
Timer shows no LAST run
If LAST is blank and you just enabled the timer, that is normal — it has not fired yet. Run sudo systemctl start system-backup.service to test. If Persistent=true is set and the system was recently booted, a missed run should have fired automatically.
Service exits with failure
Run journalctl -u system-backup.service -n 100 and look at the final lines. Common causes: the backup destination is not mounted (Requires=mnt-backup.mount and After=mnt-backup.mount in the service [Unit] section will gate it), or the script has a path problem. Test the script directly as root first.
OnCalendar expression not matching expectations
Always run systemd-analyze calendar "your expression" before deploying. Timezone matters — systemd uses the system timezone by default. Prefix with TZ=UTC in the expression if you need UTC scheduling regardless of local time.
Timer not surviving reboot
Confirm you ran systemctl enable on the timer unit, not just start. Check systemctl is-enabled system-backup.timer — it should return enabled.
Frequently asked questions
- Can I use a systemd timer instead of anacron for laptops that aren't always on?
- Yes. Set Persistent=true in the [Timer] section. If the system is off when the timer would have fired, systemd runs the job shortly after the next boot, mirroring anacron's catch-up behavior.
- What is RandomizedDelaySec and why should I use it?
- It adds a random offset between 0 and the specified duration after the scheduled time. On multiple machines sharing a backup destination, this prevents them from all starting simultaneously and saturating the network or storage.
- How does OnCalendar syntax compare to cron syntax?
- OnCalendar uses the format 'DayOfWeek Year-Month-Day Hour:Minute:Second' with wildcards as *. It is more readable than cron's five-field format and supports ranges, steps, and named shorthands like 'weekly'. Use systemd-analyze calendar to test any expression.
- Do I need to enable the service unit as well as the timer?
- No. The service unit should have no [Install] section and must not be enabled. The timer activates it directly. Enabling the service would cause systemd to try to start it at boot outside of the timer schedule.
- How do I run a backup timer as a non-root user?
- Place unit files in ~/.config/systemd/user/, use systemctl --user to manage them, and run loginctl enable-linger on that account so the user session persists when logged out.
Related guides
Back Up Linux with Borg or restic
Set up encrypted, deduplicated backups with BorgBackup or restic: local and remote repos, retention pruning, restoring files, and systemd timer scheduling.
How to Check Disk Health with SMART
Learn to use smartctl to read SMART attributes, run drive self-tests, and identify early warning signs of HDD and SSD failure before data loss occurs.
Debug systemd Units that Won't Start
Learn a repeatable workflow to debug systemd services that won't start: status output, journalctl, systemd-analyze verify, and safe override.conf patches.
Linux Server Disaster Recovery Checklist
A practical Linux server disaster recovery checklist: what to back up, RTO/RPO planning, immutable off-site copies, automated restore drills, and verification.