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:
| Symptom | Most likely cause | Jump to |
|---|---|---|
Timer not in list-timers, or NEXT is n/a | Not enabled, or bad OnCalendar | §1 / §3 |
| Edits seem to have no effect | Missing daemon-reload | §2 |
| LAST shows a run, but nothing happened | The paired service failed | §4 |
| Works, then randomly stops | User timer without lingering | §5 |
| Runs minutes later than scheduled | AccuracySec / 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]section —enablethen does nothing useful. A timer needsWantedBy=timers.targetunder[Install]. - The timer was masked (
systemctl statusshowsmasked) — typically by a package manager or an admin.systemctl unmask backup.timerfirst.
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.timer → backup.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 inExecStartand for every file the script touches. - Missing environment — your shell profile is not loaded.
PATHis minimal;nvm,pyenv, rbenv shims and exported secrets don’t exist. SetEnvironment=/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 withType=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=trueruns 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.AccuracySecdefaults to 1 minute, andRandomizedDelaySecadds jitter — both make runs later than the literalOnCalendartime. SetAccuracySec=1swhen you need punctuality.WakeSystem=truecan 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.