Skip to content
LastPing
GUIDE · BASH · FREE

Monitor bash scripts: trap EXIT to ping on success or failure

Define a _finish function that checks $? and either curls the success URL or POSTs to /fail, then add trap '_finish' EXIT. Works with set -e: any failing command exits the script, the trap fires, and the exit-code check sends the right ping. Absence detection catches SIGKILL and scripts that never start. Free, no monitor cap.

Your assistant can do this for you

Monitor my Bash scripts with LastPing

On this page
  1. The snippets
  2. What this catches
  3. Questions people ask

The snippets

Create a monitor in the console, set the schedule plus a grace window, copy the ping URL, and paste the trap header at the top of your script.

Minimal trap: exit code only

Works with any bash script. set -euo pipefail makes every non-zero command exit immediately so the trap always fires on the first error:

#!/usr/bin/env bash
set -euo pipefail

PING="${LASTPING_URL:-https://ping.lastping.dev/<your-monitor-id>}"

_finish() {
  local ec=$?
  if [ "$ec" -eq 0 ]; then
    curl -fsS --retry 3 --max-time 10 "$PING" >/dev/null
  else
    curl -fsS --retry 3 --max-time 10 \
      -X POST \
      --data "script exited with code $ec" \
      "$PING/fail" >/dev/null || true
  fi
}
trap '_finish' EXIT

# ── your job starts here ──────────────────────────────────────
echo "Starting backup..."
rsync -a /data/ /backup/
echo "Done."

Capture stderr for the fail body

Without set -e, redirect stderr to a temp file. If the script exits non-zero, POST the contents of that file to /fail so the alert body contains the actual error output, not just an exit code:

#!/usr/bin/env bash
set -uo pipefail   # -e omitted so we control the exit

PING="${LASTPING_URL:-https://ping.lastping.dev/<your-monitor-id>}"
ERR_LOG=$(mktemp)

_finish() {
  local ec=$?
  if [ "$ec" -eq 0 ]; then
    curl -fsS --retry 3 --max-time 10 "$PING" >/dev/null
  else
    # POST stderr (up to 10,000 bytes) to /fail
    curl -fsS --retry 3 --max-time 10 \
      -X POST \
      --data-binary "@${ERR_LOG}" \
      "$PING/fail" >/dev/null || true
  fi
  rm -f "$ERR_LOG"
}
trap '_finish' EXIT

# capture stderr for the fail body
exec 2>"$ERR_LOG"

# ── your job ──────────────────────────────────────────────────
some_command_that_might_fail || exit 1

Optional: /start ping for overrun detection

A /start ping arms the hung-job alarm: if no success ping arrives inside the grace window an incident opens even if the script just hangs rather than crashing:

PING="${LASTPING_URL:-https://ping.lastping.dev/<your-monitor-id>}"
RID="$$-$(date +%s)"   # PID + unix timestamp

curl -fsS --retry 3 --max-time 10 "$PING/start?rid=$RID" >/dev/null

_finish() {
  local ec=$?
  if [ "$ec" -eq 0 ]; then
    curl -fsS --retry 3 --max-time 10 "$PING?rid=$RID" >/dev/null
  else
    curl -fsS --retry 3 --max-time 10 \
      -X POST --data "exited $ec" \
      "$PING/fail" >/dev/null || true
  fi
}
trap '_finish' EXIT

Why SIGKILL can't be trapped

trap catches EXIT, ERR, TERM, and INT, but not SIGKILL. An OOM-killed process sends no ping. That is exactly what absence-detection handles: the missing ping itself opens the incident after the grace window. See the cron monitoring guide for the scheduler side and the Python guide for a language-level comparison. Also available: LastPing MCP server for AI-agent provisioning.

What this catches

The trap reports what it can; the silence reports the rest.

  • A failing command (set -e): bash exits immediately on any non-zero command, the trap fires, and the exit code goes to /fail.
  • A non-zero explicit exit: exit 1 triggers the EXIT trap; the body can include the captured stderr output.
  • SIGTERM: the EXIT trap fires on graceful shutdown signals, giving you a chance to report before the process ends.
  • SIGKILL and OOM kills: untrapable; handled by absence detection: no ping reaches LastPing within the schedule+grace window → incident.
  • The script never ran: missing cron entry, missing executable, dead host. Absence → incident.

Questions people ask

Front-loaded answers: the most important fact first.

  • How do I monitor a bash cron script?

    Set PING to your LastPing URL, define a _finish function that checks $? and either curls the success URL or POSTs to /fail, then add trap '_finish' EXIT before your job code. set -e causes bash to exit on any non-zero command, so the trap fires on the first failure.

  • Does trap EXIT work with set -e?

    Yes. With set -e any failing command exits the script immediately; bash still runs the EXIT trap when the script exits, regardless of the cause. The $? inside the trap reflects the exit code that triggered the exit, and the trap handler checks it to determine whether to send the success or failure ping.

  • Can I send the actual error message, not just the exit code?

    Yes. Without set -e (drop -e from the shebang line so you control exits manually), redirect stderr to a temp file with exec 2>$ERR_LOG before your job code. In the _finish trap, if the exit code is non-zero, POST the contents of that file to the /fail endpoint using curl --data-binary @$ERR_LOG. Up to 10,000 bytes are stored with the ping event.

  • What if the script is killed with SIGKILL?

    SIGKILL cannot be caught or trapped: bash does not run EXIT handlers when killed with SIGKILL. Absence-detection monitoring handles this: the missing ping itself opens an incident after the grace window, without any trap code running.

  • Is bash cron monitoring free?

    Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.

FREE FOR INDIVIDUALS · FULLY HOSTED

SIGKILL can't be trapped: absence detection is the safety net.

Five lines now beat a silent cron job later. First alert wired up in about a minute, and LastPing monitors itself the same way.