Skip to content
shellhacks.dev
Cron and Scheduling

How to Monitor Timers and Their Last Runs Step by Step

List systemd timers with systemctl list-timers, read the columns, check the last run result in the journal, find failed timers and build a simple check.

6 min read
Summarize in AI
Illustration for "How to Monitor Timers and Their Last Runs Step by Step": a terminal session, a scrolling log, gears.

A timer is easy to forget once it is set. The questions that come up months later are always the same: is it still scheduled, when did it last run, and did the run succeed. systemd answers all three, in different places. This post maps the questions to the commands, and ends with a small check you can put in monitoring. It follows writing OnCalendar timers. Examples use systemd 257 on Debian 13.

The list

Shell
systemctl list-timers --all --no-pager
Text
NEXT                        LEFT     LAST                        PASSED  UNIT                ACTIVATES
Wed 2026-11-04 03:30:00 UTC 15h left Tue 2026-11-03 03:30:12 UTC 8h ago  backup.timer        backup.service
Tue 2026-11-03 13:00:00 UTC 4h left  Tue 2026-11-03 12:00:08 UTC 56min ago update-check.timer update-check.service
-                           -        -                           -       old-job.timer       old-job.service

The last row has no next run: the timer is loaded but inactive, perhaps stopped or never enabled. Without --all, inactive timers are hidden, which is why a forgotten job vanishes from the default view. NEXT includes any random delay already chosen, so it can differ from the calendar expression by a few minutes.

Did the job work

Shell
systemctl status backup.service --no-pager
journalctl -u backup.service --since yesterday --no-pager
systemctl show backup.service -p Result -p ExecMainStatus -p ExecMainExitTimestamp
Text
Result=success
ExecMainStatus=0
ExecMainExitTimestamp=Tue 2026-11-03 03:41:55 UTC

The timer only tells you it triggered the service. A oneshot service shows inactive (dead) after a good run and failed after a bad one, with the exit status in the status output. The journal holds everything the script printed, including the error that caused a failure. A timer showing a recent LAST with a failed service is the typical bad case that cron hid in an unread mail.

Find the failures

Shell
systemctl --failed --no-pager
systemctl list-units --type=service --state=failed --no-pager
sudo systemctl reset-failed backup.service

A failed service stays in the failed state until its next successful run or until you reset it. Listing failures is a check you can run from a script and send to the alert channel; see basic alerting. An OnFailure= hook, as in converting cron jobs, pushes the notice instead of waiting for you to look.

A monitoring check

check-timers.shShell
#!/usr/bin/env bash
status=0
for t in backup update-check; do
  last=$(systemctl show "$t.timer" -p LastTriggerUSec --value)
  result=$(systemctl show "$t.service" -p Result --value)
  if [ -z "$last" ] || [ "$last" = "n/a" ]; then
    echo "$t: never triggered"; status=1
  elif [ "$result" != "success" ]; then
    echo "$t: last result $result"; status=1
  fi
done
exit $status

A timer that never triggered and one whose last run failed are the two states worth an alert. For a missed-run check that does not depend on the machine, use a heartbeat as in dead man switch monitoring.

User timers

Shell
systemctl --user list-timers --all
journalctl --user -u mytask.service
loginctl show-user "$USER" -p Linger

User timers belong to the user's systemd instance, which stops when the last session ends unless lingering is enabled (loginctl enable-linger USER). A "missing" user timer is often just a stopped user manager.

Where this does not apply

The list shows only systemd timers; cron jobs live in crontabs, and at jobs in the queue, as described in at and atq. On a system with many timers, filter by name with systemctl list-timers 'backup*'.

Reading the columns correctly

The table printed by systemctl list-timers has six columns. NEXT is the time of the next activation, LEFT the time remaining, LAST the time of the last activation, PASSED the time since then, UNIT the timer, and ACTIVATES the service it starts. By default it lists only active timers, and --all adds inactive ones, which is how you find a timer that exists but is not enabled. A timer with a LAST of "n/a" has never fired since boot or since its stamp was removed, and for a daily timer on a machine that has been up for a day, that is a sign of trouble. A NEXT of "n/a" means the timer has no future elapse, as with a monotonic timer that has already fired once and has no repeat, or a calendar expression that can never match. Check those two columns first, since they separate a healthy timer from a dead one at a glance.

The list shows when the timer last fired, not whether the job succeeded. A timer can fire on time every night while the service it starts fails every time, and only the service's status tells you that.

Finding failures in the service

systemctl status name.service shows the result of the last run: the exit status, the time it ended and the last lines of output. systemctl --failed lists every unit in a failed state across the machine, which is a one-command health check for scheduled work. For history, journalctl -u name.service --since yesterday shows each run's output with timestamps, and -p err filters to errors. The property Result from systemctl show name.service gives a short word such as success or exit-code, which is handy in scripts. Services of type oneshot show as inactive between runs, which looks odd, and is normal; the last result is what you want, not the current activity state. A job that fails silently because its script exits with zero after an error will look healthy here, so make scripts return honest exit codes.

Turning the checks into monitoring

A small script can run systemctl is-failed on each unit you care about, or systemctl --failed --no-legend over all of them, and send an alert when the list is not empty. Add a staleness check for timers that must run regularly: compare the last trigger time, read with systemctl show -p LastTriggerUSec name.timer, with the current time, and alert if it is older than the schedule allows. Use OnFailure= in the service to start a notification unit at the moment of failure, with %n passing the failed unit's name, so you hear about it immediately and not at the next check. For jobs that must be proven to have run, add a heartbeat ping at the end of the script. User timers, started with systemctl --user, run only while the user has a session unless you enable lingering with loginctl enable-linger, which is a common reason a personal timer never fires on a server.

Cleaning up and keeping the list honest

Over time machines collect timers from packages, and knowing what each does keeps surprises away. List them with the activated service, read each unit's description with systemctl cat, and disable the ones you do not need, using systemctl mask if a package keeps turning them back on. Package timers such as the ones for log rotation, man page database updates and package cache cleaning are normal, and the list also shows timers from software you installed and forgot about. Compare the list across machines that should be alike, to find the one with an extra or missing timer. A short inventory of expected timers per role, checked by a script, makes drift visible, and fits well with the monitoring check described above. Save the output of the list command from a healthy day for each machine, so that a later comparison shows at once which timer appeared, vanished or stopped firing after an upgrade.

See systemctl list-timers and journalctl(1).

Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.