PulseWatchPulsewatch

Exit codes and the warn state

Report the exit code with the ping and PulseWatch decides what it means, using rules you control. Without this there are two answers, worked and did not work, and most jobs have a third: it finished, and something was wrong. A backup that skipped four locked files is neither a clean run nor an outage, and filing it as either is how a real problem goes unnoticed for a month.

Reporting the code

Three ways, all equivalent. The path form is the one to reach for.

FormExample
Path segment/ping/<token>/exit/100
Query parameter/ping/<token>?exit=100
JSON body{"exit_code": 100}

Valid codes are 0 to 255. A malformed value is dropped rather than failing the ping, so /exit/ with nothing after it is treated as a plain ping. If your rule seems to be ignored, check the code actually arrived.

Setting it up

  1. Create a heartbeat monitor and copy its ping URL. Nothing here needs a paid plan; the defaults below apply to every account.
  2. Change the job to report its code. The important detail is the separator: ; runs the curl whatever happened, && runs it only on success, which means a failing job sends nothing and shows up as silence.
    bash
    # The whole change: report the code instead of only reporting success.
    # Note the ; not &&. With &&, a failing job sends nothing at all.
    0 3 * * * /opt/backup.sh; curl -fsS -m 10 --retry 5 \
      "https://pulsewatcher.up.railway.app/ping/<token>/exit/$?"
  3. Let it run once. On the monitor page the run now reads exit 0 (ok) rather than just success.
  4. Open the Exit codes card on the monitor page. This is a card of its own, below the metric rules, not a field inside the monitor form.
  5. Add a rule: from and to (both 100 for a single code), an outcome, and a label. The label is what the alert says, so write the meaning rather than repeating the number.
  6. Confirm it worked: send a test ping with that code and watch the run. It should show your label and the outcome you chose.
    bash
    curl -fsS "https://pulsewatcher.up.railway.app/ping/<token>/exit/100"
    Rules are cached for up to a minute on the ping path, so an edit can take one more ping to take effect.

Worked example: a backup that skips locked files

The job exits 100 when it completed but had to skip something.

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

skipped=0
for f in /data/*.sql; do
  if ! restore "$f"; then
    skipped=$((skipped + 1))
  fi
done

if [ "$skipped" -gt 0 ]; then
  echo "skipped $skipped locked files" >&2
  exit 100          # completed, with problems
fi
exit 0

With the default rules and no configuration at all, 100 is already warn with the label completed with issues. Adding a rule of your own changes the label to something specific:

text
from 100  to 100  outcome warn  label "skipped locked files"

The monitor stays up, no incident opens, nobody is paged, and the next morning the digest says:

text
Subject: Nightly restore: 3 warnings in the last 24 hours

3 in the last 24 hours. Nothing here took the monitor down.

03:00  exit 100 (skipped locked files): warn
04:00  exit 100 (skipped locked files): warn
05:00  exit 100 (skipped locked files): warn

The defaults you already have

Every account starts with a set of rules covering the conventions. You only add a rule where your job means something different.

CodesOutcomeLabel
0successok
1 to 63downfailed
64 to 78downsysexits.h, named individually: usage error, data format error, cannot open input, and so on
100 to 119warncompleted with issues
124, 125downtimeout (GNU timeout) and timeout command failed
126, 127downcommand found but not executable, command not found
129 to 165downkilled by signal, with 137 named as SIGKILL, likely out of memory

The 100 to 119 range is the recommended convention. Changing exit 1 to exit 100 in a script is the cheapest way to start using the warn state, and it needs no configuration at all.

What each outcome does

OutcomeMeansEffect
successThe run was cleanNothing is sent. The monitor is up.
warnThe run completed, with problemsNever pages and never opens an incident. Counts as available, because the work got done. Delivered on the warn schedule.
downThe run failedPages, opens an incident, repeats if you set a repeat interval.
ignoreThis code carries no informationThe ping falls back to what it already said: a plain ping stays success, a /fail ping stays down. Use it to silence a code your wrapper emits for its own reasons.

Which rule wins

Rules can live on a monitor, on a fleet, or on the account as a default. When several match a code, the most specific one wins:

  1. A rule scoped to this monitor beats an account default.
  2. Among equally scoped rules, the narrower range wins.

That is why the broad 129 to 165 killed by signal rule and the specific 137 SIGKILL, likely out of memory rule coexist without either knowing about the other. Exit 137 gets the OOM label; exit 140 gets the general one.

A non-zero code on a plain success ping is treated as authoritative, and the code wins. A wrapper that pings unconditionally would otherwise file every failed run as a success.

How warnings are delivered

A warn never pages. What varies is when you hear about it.

warn_deliveryBehaviour
digest_dailyThe default. One summary per monitor per day, listing the warnings since the last one. A day with no warnings sends nothing, and a quiet day does not consume the window, so the first warning after a quiet spell is not delayed another day.
immediateOne message per warning, as it happens. Still never pages, still opens no incident.
silentRecorded on the monitor page and in the run history, never sent anywhere.

A warning that never stops becomes an alert

warn_escalate_after defaults to 3 and takes a value from 1 to 100. After that many consecutive warns the monitor goes down with reason persistent_warnings, and that one does page and does open an incident.

text
03:00  exit 100   up   -> warn   digest, no page
04:00  exit 100   warn -> warn   digest, no page
05:00  exit 100   warn -> down   PAGES, opens an incident
                                 reason: persistent_warnings

Without escalation, warn would be a way to make a real failure permanently invisible. A backup that has skipped files every night for three weeks is not having a good month.

Every field

FieldTypeRangeEffect
code_minint0 to 255Start of the range this rule matches.
code_maxint0 to 255End of the range. Set both to the same number for a single code.
outcomeenumsuccess, warn, down, ignoreWhat the code means.
labeltextup to 80 charactersAppears in the alert, so it reads "exit 100 (skipped locked files)" rather than "exit 100".
scopeownermonitor, fleet, or account defaultA rule with no monitor and no fleet applies to everything on the account that has no more specific rule.

Exit-code rules are available on every plan, including Free. warn_delivery and warn_escalate_after are fields on the monitor, documented with the rest of the monitor configuration.

Variations

Nothing to configure

Use exit 100 in your scripts and stop. The defaults already treat it as a warning.

Codes that mean specific things

100 skipped files, 101 partial sync, 102 upstream unavailable, each with its own label. The alert then names the problem rather than a number.

A code that should page

Map it to down. Data corruption detected is not a warning even if the job exited politely.

Silencing a wrapper

Some runners exit non-zero for reasons of their own. Map that code to ignore and the ping falls back to what it already said.

An account-wide convention

Set the rules with no monitor selected and they apply everywhere, including to monitors created later. Override per monitor where a job disagrees.

What can go wrong

The job failed and PulseWatch heard nothing

The line uses &&, so the ping only runs on success. Use ; with /exit/$?.

$? is always 0

Something ran between the job and the curl and reset it. Capture it immediately: job.sh; code=$?; curl .../exit/$code.

A pipeline hides the failure

job.sh | tee log reports tee's exit code, not the job's. Use set -o pipefail, or read ${PIPESTATUS[0]}.

The rule appears to be ignored

Either a narrower rule matched first, or the code never arrived, or the edit is inside the one-minute cache window. Check the run: it shows the code it actually received.

Warnings are arriving as pages

Consecutive warns crossed warn_escalate_after. That is the escalation working. Fix the underlying warning, or raise the threshold if three in a row is genuinely normal for that job.

Related

  • Metric rules when the job exits 0 and the number it produced is wrong. An exit code says how the run ended; a metric says what it did.
  • Reporting errors to send the stack trace along with the failure.
  • States and reasons for exactly what warn does to uptime and incidents.
  • Fleets to declare one set of exit-code rules for hundreds of machines.