GUIDE · OVERRUN DETECTION · FREE

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.

#!/bin/sh — the full wrapper, handles start + exit-code + hung detection URL="https://ping.lastping.dev/<your-monitor-id>" curl -fsS -m 10 --retry 3 -o /dev/null "$URL/start" /usr/local/bin/backup.sh rc=$? curl -fsS -m 10 --retry 3 -o /dev/null "$URL/$rc" # 0 = success, anything else = failure exit $rc

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.

Problem 01

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.

Problem 02

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.

Problem 03

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.

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.

https://ping.lastping.dev/<your-monitor-id>

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.

#!/bin/sh # /usr/local/bin/lastping-run.sh — universal wrapper URL="https://ping.lastping.dev/<your-monitor-id>" # Arm overrun detection curl -fsS -m 10 --retry 3 -o /dev/null "$URL/start" # Run the actual job "$@" rc=$? # Report result: /0 = success, /1 (or any non-zero) = failure curl -fsS -m 10 --retry 3 -o /dev/null "$URL/$rc" exit $rc
# crontab -e — use the wrapper for any job 0 3 * * * /usr/local/bin/lastping-run.sh /usr/local/bin/backup.sh

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:

#!/bin/sh URL="https://ping.lastping.dev/<your-monitor-id>" MAX_SECONDS=3600 # kill after 1 hour curl -fsS -m 10 --retry 3 -o /dev/null "$URL/start" timeout "$MAX_SECONDS" "$@" rc=$? # exit code 124 means timeout -> report as failure curl -fsS -m 10 --retry 3 -o /dev/null "$URL/$rc" exit $rc

Python / Node.js: inline start + success pings

For in-process jobs, send the pings from code:

# Python import urllib.request, sys PING = "https://ping.lastping.dev/<your-monitor-id>" def run(): urllib.request.urlopen(f"{PING}/start", timeout=10) try: do_work() urllib.request.urlopen(PING, timeout=10) except Exception: urllib.request.urlopen(f"{PING}/fail", timeout=10) raise
// Node.js const PING = 'https://ping.lastping.dev/<your-monitor-id>'; async function run() { await fetch(`${PING}/start`); try { await doWork(); await fetch(PING); } catch (err) { await fetch(`${PING}/fail`); throw err; } }

What the start/success pattern catches

Three signals, three failure modes covered — all with a single monitor.

  • Job never started — no /start ping in the expected window → incident opens. Crontab lost, machine down, scheduler misconfigured — all caught.
  • Job started but is hung/start arrived; no success within grace → incident opens while the job is still running. Overrun detection fires.
  • Job failed/fail or 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 exposes all three signals as MCP tools.


Questions people ask

Front-loaded answers — the most important fact first.

  • How do I detect a hung job?

    Send a /start ping 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.sh to 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 and fully hosted with no monitor cap and no paid tier. See how it compares to Healthchecks.io and Cronitor.

FREE · FULLY HOSTED · NO PAID TIER

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.