This is the script from the auto-restart guide, on its own page
Why Restart=on-failure should be your first answer and a cron watchdog your last, what is-active cannot see, how start-limit-hit turns a restart loop into a permanent failure, and why a probe-driven restart destroys the evidence you needed — all of that is in the full guide: Auto-Restart a Stopped Service on Linux. This page is the copy-paste home for the watchdog and what it printed when run here.
The twelve-line watchdog — is-active || start, every minute — is correct for a well-behaved daemon on a box you watch. It fails in four specific ways the moment it meets production: it restarts a unit that does not exist forever, it never notices a service that is running but hung, it cannot start a unit that systemd has already given up on, and it emails you sixty times for a one-hour outage. This script is the same idea with those four holes closed.
The Script
Save as service-watchdog.sh. The unit name is the first argument; everything after it is an optional probe command that must exit 0 only when the service is actually answering.
What Happens on Each Run?
In order: take a flock (a second run exits if the first is still inside a slow probe), refuse a unit whose LoadState is not-found, skip if the maintenance flag exists, then branch on ActiveState. active runs the probe and restarts only on failure. inactive or failed goes through start_unit, which clears start-limit-hit with reset-failed at most MAX_RESETS times per outage. Every start is followed by verify: wait SETTLE_SECONDS, then require both is-active and the probe. The verdict — up, down, stuck — lands in a state file, and ALERT_CMD runs only when the verdict differs from the previous one.
What Does It Look Like Live?
Every path below was run on this machine on 2026-09-10, as an unprivileged user, with the state, log, and alert outputs pointed at scratch paths:
Against the running cron unit, the first run printed nothing, exited 0, wrote up to /tmp/wd/cron.state, and created no alerts.txt. A healthy unit on a first run is not a recovery, so it is silent. A maintenance flag and a typo in the unit name each produce one log line:
The second of those exited 2. Note what did not happen: no systemctl start cronn, and no second attempt a minute later.
Stopping a system unit here needs polkit admin authentication that a non-interactive session cannot supply, so the restart path was exercised against a user-scope unit (wd-demo.service, ExecStart=/bin/sleep 1000000) with systemctl shimmed to systemctl --user on PATH, and a probe of pgrep -xf '/bin/sleep 1000000'. Stop the unit, run the watchdog: two log lines, three seconds apart, exit 0, and the unit is back:
/tmp/alerts.txt received exactly two messages, one per state change:
The run after that, with the unit healthy, printed nothing and the alert file stayed at four lines. That is the whole alerting contract: a transition sends one message, steady state sends none.
How Do I Schedule It?
From root's crontab, so systemctl start never needs a sudo that cron cannot answer:
Everything tunable is an environment variable with a default, so the crontab line stays readable:
| Variable | Default | What it controls |
|---|---|---|
STATE_DIR | /var/tmp/service-watchdog | verdict, reset counter, lock, and maintenance flag per unit |
LOG_FILE | /var/log/service-watchdog.log | every intervention, timestamped |
MAX_RESETS | 3 | reset-failed calls allowed per outage before it stops trying |
SETTLE_SECONDS | 3 | pause between start and the verify step |
ALERT_CMD | empty | reads the message on stdin — mail -s …, a webhook curl, cat >> file |
Set ALERT_CMD in the crontab line itself (ALERT_CMD='mail -s "watchdog" you@example.com' /usr/local/sbin/…). The Slack webhook snippet is a drop-in for the mail version.
One thing this script does on purpose that you may not want: on a service that is active but failing its probe, it restarts immediately. The restart is stop plus start, and it throws away the process state that would have told you why it hung. The second time the same unit needs a probe-driven restart, capture wchan, open files, and socket queues first — the commands are in Diagnosing a Hung Process.
The lock, the settle-and-verify step, and the alert-on-transition pattern are the same three pieces every unattended script ends up needing. The Production Bash Toolkit ships them once as bashlib.sh so the next watchdog is a config change, not a rewrite.
Frequently Asked Questions
How is this different from the 12-line restart-service-if-stopped script?
The short script does one thing: if is-active fails, run start. This one handles the cases that break that loop in production — a unit that does not exist (is-active exits 4, so the short script restarts it forever), a service that is active but hung (needs a probe), start-limit-hit after a crash loop (needs a bounded reset-failed), a deliberate stop (maintenance flag), overlapping cron runs (flock), and alert spam (one message per state change).
Why does the watchdog say "no such unit" and exit 2?
It checked systemctl show -p LoadState and got not-found: the unit name is wrong. systemctl is-active exits 4 for a missing unit, and a naive watchdog reads that as "down" and tries to start it every minute forever. Exit 2 with one log line stops that. Fix the name.
How do I stop the watchdog from restarting a service I stopped on purpose?
Touch the maintenance flag before you stop the service: touch /var/tmp/service-watchdog/nginx.maintenance (the path is STATE_DIR/UNIT.maintenance). Every run logs maintenance flag present, skipping and exits 0 until you remove the file. Remove it afterwards, or the watchdog is off for good.
What is the probe command and do I need one?
Anything after the unit name is the probe: a command that exits 0 only when the service actually answers — curl -fsS --max-time 5 http://127.0.0.1/ for nginx, pg_isready for PostgreSQL. Without one, active is the whole verdict, and a hung process that is still alive is never restarted. Add a probe for anything that serves requests.
Why did I only get two alerts for an outage that lasted an hour?
By design. The verdict (up, down, stuck) is written to a state file and ALERT_CMD runs only when it changes. An hour-long outage is one DOWN message when it starts and one RECOVERED when it ends, not sixty CRITICAL emails from sixty cron ticks.
Part of the bash snippets collection
Related Scripts
- Restart a Service If It Stopped — the twelve-line version this one grew out of
- Prevent Overlapping Cron Jobs with flock — the lock the watchdog takes, on its own
- Check If Website Is Up — the HTTP probe as a standalone script