Troubleshooting · systemd

systemd timer not running? Diagnose it in five steps

Why a systemd timer doesn't fire or trigger — not enabled, OnCalendar syntax, missing daemon-reload, failing service unit, user timers without lingering — and the fix for each.

systemd timers are the modern replacement for cron on Linux — with better logging and more failure modes to know about. Work through these in order.

30-second diagnosis:

SymptomMost likely causeJump to
Timer not in list-timers, or NEXT is n/aNot enabled, or bad OnCalendar§1 / §3
Edits seem to have no effectMissing daemon-reload§2
LAST shows a run, but nothing happenedThe paired service failed§4
Works, then randomly stopsUser timer without lingering§5
Runs minutes later than scheduledAccuracySec / RandomizedDelaySec§5

1. Is the timer actually enabled and started?

The classic: the unit files exist, but nobody started the timer.

systemctl list-timers --all          # is your timer in the list? NEXT set?
systemctl status backup.timer        # "Active: active (waiting)" is what you want
sudo systemctl enable --now backup.timer

enable alone makes it start on next boot; enable --now starts it immediately. If NEXT shows n/a, the timer is loaded but has no valid trigger — usually a syntax problem (§3).

Two related traps:

  • The timer is enabled but the unit file has no [Install] sectionenable then does nothing useful. A timer needs WantedBy=timers.target under [Install].
  • The timer was masked (systemctl status shows masked) — typically by a package manager or an admin. systemctl unmask backup.timer first.

2. Did you daemon-reload after editing?

systemd reads unit files once. Edits to backup.timer or backup.service do nothing until:

sudo systemctl daemon-reload
sudo systemctl restart backup.timer

This bites hardest with configuration management (Ansible, Puppet) that writes unit files but doesn’t notify a reload — the file on disk and the loaded unit silently diverge. systemctl show backup.timer -p NeedDaemonReload tells you if the loaded version is stale.

3. Validate the OnCalendar expression

OnCalendar is not cron syntax. 0 3 * * * silently means something else or fails to parse. The format is DayOfWeek Year-Month-Day Hour:Minute:Second:

[Timer]
OnCalendar=*-*-* 03:00:00        # daily at 03:00
OnCalendar=Mon *-*-* 09:00:00    # Mondays at 09:00
OnCalendar=*-*-* *:0/15:00       # every 15 minutes
OnCalendar=Sat,Sun *-*-* 10:00   # weekends at 10:00

Always check with the built-in validator, which also prints the next elapse:

systemd-analyze calendar "Mon *-*-* 09:00:00"

Failed to parse calendar specification in journalctl -u backup.timer means the timer never scheduled anything. If you’re translating from a crontab, the common schedule reference shows each cron expression’s meaning in plain words — translate the meaning, not the syntax.

Timers also default to the system timezone (unlike GitHub Actions or Vercel, which force UTC) — worth remembering when a unit written on your laptop deploys to a UTC server.

4. The timer fired — but the service failed

list-timers showing a recent LAST only means the timer worked. The work happens in the paired service unit (backup.timerbackup.service, unless Unit= points elsewhere):

systemctl status backup.service      # Result: exit-code? oom-kill?
journalctl -u backup.service -n 50   # the actual output and errors

The usual suspects, in order of frequency:

  • Relative paths — units run from /, not from your home directory. Use absolute paths in ExecStart and for every file the script touches.
  • Missing environment — your shell profile is not loaded. PATH is minimal; nvm, pyenv, rbenv shims and exported secrets don’t exist. Set Environment= / EnvironmentFile= in the unit.
  • Permissions — the service runs as User= (default root for system units); files created by your interactive user may not be readable/writable.
  • Type= mismatch — a script that daemonizes with Type=oneshot (or the reverse) makes systemd kill or mis-track the process.

5. User timers stop when you log out

A timer installed with systemctl --user only runs while you have a session — unless lingering is enabled:

loginctl enable-linger $USER

Without it, your timer dies on logout and resumes at next login — which looks exactly like “randomly stops running.”

Also worth knowing:

  • Persistent=true runs a missed schedule at next boot — essential for machines that aren’t always on. Without it, a timer scheduled for 03:00 on a laptop that was asleep simply skips.
  • AccuracySec defaults to 1 minute, and RandomizedDelaySec adds jitter — both make runs later than the literal OnCalendar time. Set AccuracySec=1s when you need punctuality.
  • WakeSystem=true can wake a suspended machine for the timer — the default is to wait until the next resume.

The deeper problem: the journal doesn’t page you

Everything above lands in journalctl — which is exactly where nobody looks until something has been broken for weeks. A timer that stops scheduling, a service that starts failing, a machine that was off at trigger time: all silent.

Close the loop with a heartbeat ping on success:

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStartPost=/usr/bin/curl -fsS https://ping.steadycron.com/<your-ping-token>

ExecStartPost only runs when ExecStart succeeds — so a failed backup, a dead timer, or a powered-off machine all produce the same signal: a missed ping and an alert to email, Slack, Discord, or Telegram within your grace period. See heartbeat monitoring for grace periods and alert routing, and ping snippets for wrapper patterns that also catch failures (OnFailure= handlers included).

Frequently asked questions

Why is my systemd timer not triggering at all?

Check the basics in order: is the timer enabled and started (systemctl list-timers --all — does NEXT show a time?), did you run daemon-reload after editing, and does systemd-analyze calendar accept your OnCalendar expression? A NEXT of n/a almost always means a calendar syntax problem.

Can I use cron syntax like 0 3 * * * in OnCalendar?

No — OnCalendar is not cron. The format is DayOfWeek Year-Month-Day Hour:Minute:Second, e.g. *-*-* 03:00:00 for daily at 03:00. Validate with systemd-analyze calendar "expression", which also prints the next elapse time.

Why does my systemd timer show a LAST run but nothing happened?

The timer and the work are separate units. LAST proves the timer elapsed; the actual job is the paired service unit. Check systemctl status yourjob.service and journalctl -u yourjob.service — the service likely failed (paths, environment, permissions).

Why does my systemd --user timer stop when I log out?

User timers only run while you have an active session, unless lingering is enabled: loginctl enable-linger $USER. Without it the timer dies on logout and resumes at next login — which looks like it randomly stops.

Why does my systemd timer run at a slightly different time each day?

AccuracySec defaults to 1 minute and RandomizedDelaySec adds deliberate jitter — both make runs land after the literal OnCalendar time. Set AccuracySec=1s if you need precision; keep some jitter if many machines fire the same schedule.