$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

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: RandomizedDelaySec spreads load across a fleet without per-machine crontab edits.
  • Introspection: systemctl list-timers shows 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. Use Mon..Fri *-*-* 06:00:00 for weekday-only, or weekly / monthly as 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 anacron behavior 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

ExpressionMeaning
hourlyEvery hour on the hour
dailyEvery day at 00:00
Mon *-*-* 04:00:00Every Monday at 04:00
*-*-1 03:00:00First of every month at 03:00
*-*-* 00/6:00:00Every 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.

tested on:Ubuntu 24.04Fedora 40Debian 12Arch rolling

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