I’ve seen this go wrong when the timer unit is enabled in two places or when Persistent=true is left on a daily job. The result is a backup that starts right after boot and then again at the scheduled hour. It’s annoying, but the fix is usually a few quick edits and a reload. Below is a practical walk‑through that keeps the technical detail but feels like a coffee‑chat with a seasoned sysadmin.
1. The Anatomy of a Systemd Timer
A timer unit (*.timer) is just a lightweight scheduler that tells systemd to activate another unit—usually a .service. The two files work together:
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
Unit=backup.service
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Run backup script
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Persistent=true is the first place that can sneak in a double run. If the machine was down at 02:00, systemd will fire the job immediately after boot and then again at the next 02:00.
2. Common Causes of Double Execution
| Cause | Why It Happens | Typical Symptoms |
|---|---|---|
| Persistent catch‑up | Machine was off at the scheduled time | Two runs on boot, then again at 02:00 |
| Multiple timer units | Two timers point to the same service | Two runs every day, minutes apart |
Timer enabled in /etc/systemd/system/ and /usr/lib/systemd/system/ |
Duplicate copies of the same unit | Duplicate rows in systemctl list-timers |
Wrong OnCalendar syntax |
Ambiguous schedule | Runs at 02:00 every hour or every minute |
| Unit file reload without timer reload | Old config still active | Unexpected schedule |
Service with RemainAfterExit=yes |
Timer thinks the job is still running | Overlapping runs |
AccuracySec= set too low |
Systemd rounds to the nearest second | Two triggers within a second |
3. Diagnosing the Problem
3.1 Inspect the Timer
systemctl status backup.timer
Active: active (waiting)– normal.Triggered: 2 times– double firing.
3.2 List All Timers
systemctl list-timers --all
Look for duplicate rows:
NEXT LEFT LAST PASSED UNIT ACTIVATES
02:00:00 23h 02:00:00 1d backup.timer backup.service
02:00:00 23h 02:00:00 1d backup.timer backup.service
3.3 Check Service Status
systemctl status backup.service
If it shows Active: active (exited) and RemainAfterExit=yes, the timer might still think it’s running.
3.4 Review Journal Logs
journalctl -u backup.service -b -n 50
Close‑together timestamps confirm double execution.
4. Fixing the Most Common Scenarios
4.1 Persistent Catch‑up
If you’re fine with a boot‑time run, leave it. If you want to avoid double runs, set it to false:
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=false
Unit=backup.service
Reload and restart:
systemctl daemon-reload
systemctl restart backup.timer
4.2 Duplicate Timer Units
Search for all timer files:
find /etc/systemd/system /usr/lib/systemd/system -name '*backup.timer'
Decide which one to keep. Remove the unwanted:
rm /etc/systemd/system/backup.timer
systemctl daemon-reload
If the duplicate lives in /usr/lib/systemd/system/, mask it:
systemctl mask backup.timer
4.3 Service Stays Active
Edit the service file:
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
RemainAfterExit=no
Reload and restart:
systemctl daemon-reload
systemctl restart backup.service
Now the timer sees the job as finished before starting a new instance.
4.4 Incorrect OnCalendar
Always use the full syntax:
OnCalendar=*-*-* 02:00:00
For more complex schedules, the systemd.time documentation is worth a look.
4.5 Reloading After Changes
After any edit, run:
systemctl daemon-reload
systemctl restart backup.timer
Skipping this step keeps the old config in memory—common source of confusion.
5. A Practical Example: A Clean Setup
Below is a minimal, reproducible setup that works on Ubuntu 24.04, Debian 12, and Arch 2026.
5.1 Backup Script
#!/usr/bin/env bash
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d%H%M%S)
DEST="/mnt/backups/$(hostname)-$TIMESTAMP"
mkdir -p "$DEST"
rsync -a --delete /var/www/html/ "$DEST/"
echo "Backup finished at $TIMESTAMP" >> /var/log/backup.log
Make it executable:
chmod +x /usr/local/bin/backup.sh
5.2 Service Unit
# /etc/systemd/system/backup.service
[Unit]
Description=Daily website backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
5.3 Timer Unit
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup.service daily
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=false
Unit=backup.service
[Install]
WantedBy=timers.target
5.4 Enable and Start
systemctl daemon-reload
systemctl enable --now backup.timer
Check status:
systemctl status backup.timer
You should see Active: active (waiting) and Triggered: 0 times.
6. Advanced Troubleshooting
6.1 systemd-analyze
systemd-analyze time backup.timer
If two trigger times show up close together, double‑fire is confirmed.
6.2 “Stale” Units
Package upgrades can leave old units in /usr/lib/systemd/system/. Check:
systemctl list-unit-files | grep backup
If you see backup.timer marked enabled there, mask it as shown earlier.
6.3 Journal for Timer Events
journalctl -u backup.timer -b | grep -i "Triggered"
This lists every trigger. Duplicate timestamps mean the timer is firing twice.
6.4 Accuracy
If you set AccuracySec=1s, systemd may round to the nearest second. On some clocks, a small jump can trigger two events. Keep the default (1 minute) unless you need sub‑minute precision.
7. Security Considerations
Double runs can waste space, bandwidth, and even expose data if the destination is misconfigured. If your script touches shared resources, add a simple lock:
LOCKFILE=/var/run/backup.lock
if [ -e "$LOCKFILE" ]; then
echo "Backup already running" >> /var/log/backup.log
exit 1
fi
touch "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
# ... backup commands ...
8. Common Mistakes to Avoid
| Mistake | Fix |
|---|---|
Leaving OnBootSec= and OnCalendar= together |
Use one or the other. If you need a boot‑time run, keep OnBootSec= and set Persistent=false. |
Using OnUnitActiveSec= without a clear reference |
That schedules relative to the last activation of the referenced unit; it’s easy to mis‑calculate. |
| Forgetting to reload after editing a unit | systemctl daemon-reload is mandatory after any file change. |
Enabling a timer in both /etc/systemd/system/ and /usr/lib/systemd/system/ |
Keep only one copy in /etc/systemd/system/. |
9. Quick Checklist Before You Deploy
- Single timer file – no duplicates in
/etc/systemd/system/or/usr/lib/systemd/system/. - Correct
OnCalendarsyntax – full date‑time format. Persistent=false– unless you explicitly need catch‑up.- Service
Type=oneshot– andRemainAfterExit=no. - Reload systemd –
systemctl daemon-reload. - Enable the timer –
systemctl enable --now <timer>. - Verify –
systemctl status <timer>andjournalctl -u <service>.
10. When All Else Fails: Manual Debugging
If you still see double runs:
- Create a tiny test service that writes a timestamp to a file.
- Attach the same timer and watch the timestamps.
- If the problem persists, check the systemd GitHub issues for regressions.
11. TL;DR
- Persistent catch‑up is the most common culprit. Set
Persistent=falseif you don’t want double runs after a reboot. - Duplicate timer files cause two triggers. Keep only one copy in
/etc/systemd/system/. - Service staying active (
RemainAfterExit=yes) can confuse the timer. UseRemainAfterExit=nofor one‑shot jobs. - Always reload systemd after editing unit files.
- Use journal logs and
systemctl list-timersto confirm the behavior.
With these steps, you can eliminate the double‑fire problem and keep your backups (or any scheduled task) running reliably.
See also
- Using find + xargs to purge old log files without risking accidental deletes
- Why I Swapped Traefik for Nginx Ingress in My Home Reverse‑Proxy Stack.
- Why I Switched from Nginx to Caddy for My Home Reverse Proxy and What I Learned
- When Nginx Reverse‑Proxy Returns 502 on Your Home‑Lab HTTPS Site: A Step‑by‑Step Fix for Certificate and Path Errors
- How to audit stale SSH keys with ssh-keygen‑l and fail2ban to stop brute‑force leaks on production servers