Hung job detection: alert when a job starts but never finishes
A hung job isn't a failure — yet. The process is alive, so no error fires and no
alert sends. It's stuck on a database lock, a full-disk write that blocks forever,
or a stale NFS mount. Meanwhile, the next scheduled run can't start, and you won't
know until someone notices that the job that "always completes by 4am" is still
running at noon. The fix: send a /start ping
when the job begins and a success ping when it ends. If the success doesn't arrive
within the expected window, LastPing opens an incident while the job is still running.
Why hung jobs are the hardest failure to catch
Every monitoring tool that fires on errors is blind to a hung job. The job hasn't errored — it's just stuck.
No failure signal to detect
Exception trackers, log alert rules, and on-failure pings all need the job to exit or throw. A job stuck on a blocking call never exits, never throws, and never sends anything. The monitoring stack sees a healthy job.
The next run can't start
Most schedulers prevent a new instance from starting while the previous one is running. A job stuck for 10 hours blocks every subsequent run — compounding the problem until the hung job is found and killed.
The deadline passes unnoticed
If a job is expected to finish by 04:00 and it's still running at 08:00, whatever it was supposed to produce (a backup, a report, a data sync) is four hours late — and nobody knows unless they check manually.
The start/success ping pattern
Two pings per job: one at the start, one at the end. The gap between them is your overrun detection window. Optionally a third — a step ping per stage — narrows that window to the gap between stages.
Create a monitor with an appropriate grace window
In the console, create a heartbeat monitor with your job's schedule. Set the grace window to the maximum expected runtime, not just the expected completion delay. If your backup normally takes 5 minutes but you want to alert if it exceeds 30, set grace to 30 minutes. Copy the ping URL.
Wrap your command with a start/exit-code reporter
The /start ping arms overrun detection. The exit-code ping maps
0 to success and anything else to failure — one pattern covers both
failure detection and hung detection.
Combine with a timeout for resource safety
Overrun detection tells you the job is stuck. A timeout wrapper
kills it to prevent resource exhaustion. Together they cover both alerting and
automatic recovery:
Python / Node.js: inline start + success pings
For in-process jobs, send the pings from code:
Optional: report steps to catch a stall before the budget runs out
Overrun detection answers "did it finish in time?" — and only when the
time is up. If the job has distinct stages, a /step ping
after each one answers "is it still moving?", and answers it in
minutes. Give the run an id, reuse it on every ping, and label each
step:
Then set step_timeout_s on the monitor to the longest gap
you expect between steps. If the gap between the /start
(or the last step) and the next step exceeds it, LastPing opens a
stalled incident that names the last step that reported — so the
alert says upload, not just late.
Three things worth knowing. Step reporting is opt-in:
a monitor with no step_timeout_s behaves exactly as it did
before, and this whole step is optional. A step never extends
the run budget — a job that reports progress forever still
overruns, which is deliberate. And step_timeout_s must be
below the run budget (max_runtime_s, or the grace window
when that is unset), because above it the run would overrun first and
the stall could never fire.
What the start/success pattern catches
Four signals, four failure modes covered — all with a single monitor.
- Job never started — no
/startping in the expected window → incident opens. Crontab lost, machine down, scheduler misconfigured — all caught. - Job started but is hung —
/startarrived; no success within grace → incident opens while the job is still running. Overrun detection fires. - Job started but stopped making progress — the run reported
/steppings and then went quiet for longer than the monitor'sstep_timeout_s→ a stalled incident opens naming the last step that reported. This is the one that tells you where it wedged, and tells you in minutes rather than at the end of the run budget. Opt-in: leavestep_timeout_sunset and the monitor behaves exactly as it always has. - Job failed —
/failor a non-zero exit code ping closes the run with failure → incident opens immediately. - Recovery — next successful run closes the incident automatically. No manual reset.
- Flap protection — a transient failure that recovers within the damping window doesn't page you repeatedly.
Running GitHub Actions? The workflow_run
webhook delivers started and completed events — no YAML changes needed. See
GitHub Actions monitoring.
For the conceptual overview of why absence is a better signal than failure, see
dead-man's switch explained and
silent failure in scheduled jobs.
For AI agents that need to report their own heartbeat, the
LastPing MCP server returns ready-to-run check-in requests for all
four signals via get_ping_instructions; the check-ins themselves are plain
HTTP.
Questions people ask
Front-loaded answers — the most important fact first.
-
How do I detect a hung job?
Send a
/startping at the beginning of the job and a success ping at the end. Set the grace window to your maximum expected runtime. If the success ping doesn't arrive within the grace window, LastPing opens an incident even though the job is technically still running. This is overrun detection. -
What is the difference between a hung job and a failed job?
A failed job exits with a non-zero code and produces a signal. A hung job is still running — its process is alive — so it produces no failure signal. Error monitoring can catch failures; only overrun detection can catch hung jobs, because it uses time (the gap between start and expected completion) as the trigger.
-
What causes jobs to hang?
Common causes: (1) database lock wait — waiting for a lock held by a long-running transaction; (2) full-disk write — writes block indefinitely on some filesystems; (3) stale NFS mount — reads and writes block forever; (4) network timeout too long — waiting for a response that will never come with a default socket timeout of minutes or hours; (5) reading from stdin when not attached to a terminal.
-
Why not just set a timeout on the job?
A timeout is a good safety net — use
timeout 3600 /usr/local/bin/backup.shto kill a stuck job automatically. But a timeout alone doesn't alert you; it just kills the job and exits with code 124. Combine both: timeout for automatic cleanup, start/success pings for alerting. -
Is this free?
Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.
Know the moment a job gets stuck — not when someone notices it's late.
One wrapper script, one heartbeat monitor. Set up overrun detection in minutes — and LastPing monitors itself the same way.