Why My Custom Systemd Timer Keeps Firing Twice and How to Fix It

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

  1. Single timer file – no duplicates in /etc/systemd/system/ or /usr/lib/systemd/system/.
  2. Correct OnCalendar syntax – full date‑time format.
  3. Persistent=false – unless you explicitly need catch‑up.
  4. Service Type=oneshot – and RemainAfterExit=no.
  5. Reload systemd – systemctl daemon-reload.
  6. Enable the timer – systemctl enable --now <timer>.
  7. Verify – systemctl status <timer> and journalctl -u <service>.

10. When All Else Fails: Manual Debugging

If you still see double runs:

  1. Create a tiny test service that writes a timestamp to a file.
  2. Attach the same timer and watch the timestamps.
  3. If the problem persists, check the systemd GitHub issues for regressions.

11. TL;DR

  • Persistent catch‑up is the most common culprit. Set Persistent=false if 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. Use RemainAfterExit=no for one‑shot jobs.
  • Always reload systemd after editing unit files.
  • Use journal logs and systemctl list-timers to 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