Skip to content

Ping URLs: get alerted when scheduled work goes quiet

Each monitor has one secret URL; your job calls it when done and adds /fail on failure. Belvador alerts when the pings stop — even if your app, database or node is down.

Markdown
On this page

A ping URL monitor (a heartbeat, or dead man's switch) watches work that should happen on a rhythm — a cron job, a timer that calls your app every minute, a backup script, a CI schedule. Your job calls one secret HTTPS URL each time it runs. When the calls stop, Belvador alerts "no signal".

It needs no SDK and no header: anything that can make an HTTPS request can use it — curl, a shell crontab, Python, Node.js, PHP, Go, GitHub Actions.

Why it keeps working when your stack is down#

Your app cannot report that it has stopped. So the deadline for each ping URL monitor is kept by Belvador at the edge, not by your app and not by your node:

  • If your app, its database or its timer dies, the pings stop and Belvador alerts on time.

  • If your Belvador node is offline, ping URL monitors are still watched and still alert. Only the detailed list of recent pings waits for the node to come back.

To do this, Belvador keeps a small amount of liveness metadata outside your node: the monitor's opaque id, which signal arrived (ok, start, fail, log, exit code) and when, the deadlines and the incident state. This is the one exception to "event data stays on your node" and it is listed on the privacy page. It never includes request bodies, headers, your server's IP address or anything your job prints — output you choose to send goes to your node only (see below).

Create one#

In a project, open Monitors → Create monitor and choose Ping URL (heartbeat).

FieldMeaning
Name and slugHow the monitor appears in Belvador and in alerts
ScheduleEvery N minutes/hours (for example every 1 minute), or a cron schedule with a time zone (0 2 * * * = 02:00 every day)
Allowed delayHow long after the expected ping Belvador waits before alerting — 5 minutes by default, up to 1440
Max runtimeOnly used when your job also calls /start: a run still open after this long counts as timed out
EnvironmentOne ping URL reports for one environment (production by default). Create a second monitor for staging

After saving, Belvador shows the ping URL once. Copy it into a secret of your server or CI. Belvador keeps only a fingerprint of it, so it cannot show the URL again — if you lose it, rotate it (see below).

The monitor shows Waiting for the first ping until the first call arrives; nothing is judged before that.

Call the URL#

Text
https://ping.belvador.dev/p/bvp_<your-ping-token>             finished OKhttps://ping.belvador.dev/p/bvp_<your-ping-token>/start       started (optional; arms the max-runtime check)https://ping.belvador.dev/p/bvp_<your-ping-token>/fail        failed — alerts at oncehttps://ping.belvador.dev/p/bvp_<your-ping-token>/<0-255>     exit status: 0 = OK, 1–255 = failedhttps://ping.belvador.dev/p/bvp_<your-ping-token>/log         attach output or a note; never changes the status

GET and POST both work. A trailing / is fine.

curl#

Terminal
# Alive / finished OKcurl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token> || true# Started (optional: arms the max-runtime check)curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/start || true# Failed — alerts at oncecurl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/fail || true# Or send the job's exit status right after it: 0 = success, 1–255 = failure./nightly-job.sh; curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/$? || true

crontab#

Terminal
# Runs at 02:00; Belvador gets "started", then the job's exit status.0 2 * * * curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/start || true ; /usr/local/bin/nightly-job.sh ; curl -fsS -m 10 --retry 5 -o /dev/null https://ping.belvador.dev/p/bvp_<your-ping-token>/$? || true

Node.js, Bun or Deno#

TypeScript
const PING_URL = process.env.BELVADOR_PING_URL!; // https://ping.belvador.dev/p/bvp_<your-ping-token>/** Never lets monitoring break the job: 10 s timeout, errors ignored. */async function ping(suffix = "") {  await fetch(PING_URL + suffix, { method: "POST", signal: AbortSignal.timeout(10_000) }).catch(() => {});}await ping("/start");try {  await runJob();  await ping();} catch (error) {  await ping("/fail");  throw error;}

Python#

Python
import os, urllib.requestPING_URL = os.environ["BELVADOR_PING_URL"]  # https://ping.belvador.dev/p/bvp_<your-ping-token>def ping(suffix=""):    try:        urllib.request.urlopen(PING_URL + suffix, data=b"", timeout=10)    except OSError:        pass  # monitoring never breaks the jobping("/start")try:    run_job()    ping()except Exception:    ping("/fail")    raise

GitHub Actions#

Text
# .github/workflows/nightly.yml — store the URL as the secret BELVADOR_PING_URLjobs:  nightly:    runs-on: ubuntu-latest    steps:      - run: curl -fsS -m 10 --retry 5 -o /dev/null "${{ secrets.BELVADOR_PING_URL }}/start" || true      - run: ./scripts/nightly.sh      - if: success()        run: curl -fsS -m 10 --retry 5 -o /dev/null "${{ secrets.BELVADOR_PING_URL }}" || true      - if: failure()        run: curl -fsS -m 10 --retry 5 -o /dev/null "${{ secrets.BELVADOR_PING_URL }}/fail" || true

Send output with a ping#

Any signal can carry the job's output as a POST body — the last lines of a log, an error message, a row count. It shows under Ping output on the monitor page and inside the run it belongs to.

Terminal
# Run the job, then send its exit status with the end of its output (60 KB; the cap is 64 KB).out=$(./nightly-job.sh 2>&1); code=$?printf '%s\n' "$out" | tail -c 60000 | curl -fsS -m 10 --retry 5 -o /dev/null --data-binary @- https://ping.belvador.dev/p/bvp_<your-ping-token>/$code || true# A progress note at any time — shown with the run, never changes the statusecho "exported 1204 rows" | curl -fsS -m 10 --retry 5 -o /dev/null --data-binary @- https://ping.belvador.dev/p/bvp_<your-ping-token>/log || true
  • Up to 64 KB of text per ping. A larger body is refused with 413 and the ping is not recorded, so trim it (tail -c 60000).

  • The output is stored only on your node, the newest 100 per environment. On the way it is handled like your error events: passed straight to your node and sealed to your node's key whenever it has to wait at Cloudflare. Belvador's database and alerts never contain it.

  • Your node's scrubbing rules run on it, and any ping URL in the text is replaced before it leaves the ping service.

  • Output never decides the status: only the URL does. A body saying "error" on the plain URL is still a success.

  • Delivery to your node is best effort and never delays the 200. While your node is unreachable, pings are still judged and alerts still go out; output waits for your node within the same limits as your events.

Every recipe uses a 10-second timeout and ignores errors (|| true after each curl), so a Belvador outage can never fail your job — not even in a set -e script or a GitHub Actions step, which stops at the first failing command. Against a ping URL that never answers, one ping gives up after about 90 seconds; add --retry-max-time 30 if your job cannot wait that long.

What the statuses mean#

StatusMeaning
Waiting for the first pingCreated, no call yet
HealthyThe last call was a success and the next one is not due yet
LateThe next ping is overdue but still inside the allowed delay — no alert yet
FailingNo ping arrived within the allowed delay (Missed), the job called /fail or a non-zero exit code, or a run that called /start exceeded its max runtime
PausedNot judged until you resume, or until the pause ends

The next successful ping closes the incident and sends a recovered message.

The monitor page shows when the next ping is expected, when Belvador would alert, and the last 7 days hour by hour (OK, failed, missed).

Alerts#

Ping URL monitors use the same alert rules as other monitors: a missed or failed run opens an incident and the rule's destinations get a message; recovery sends a closing message. Don't repeat for (30 minutes by default) stops the same monitor from alerting twice in a row. Pick destinations under Alerts, including your own webhook.

Messages say "no signal received", never "your server is down": Belvador only knows that the pings stopped.

Pause for maintenance#

On the monitor page choose Pause — for 30 minutes, 1, 2, 8 or 24 hours, until a time you pick, or until you resume. While paused, missing pings never alert. Pings still count, so the history stays complete.

Rotate or reset the URL#

Treat the ping URL like a password: anyone who has it can report for this monitor (they cannot read anything). Owners and admins manage it under Ping URL on the monitor page; each action asks for your authenticator code.

  • Rotate URL issues a new URL and keeps the old one working for an overlap you choose — 1 hour, 24 hours (default), 3 or 7 days — so you can update your job without missing a ping. At most 2 URLs work at a time.

  • Stop the previous URL now ends the overlap early.

  • Reset after a leak stops every URL at once and issues a new one.

Changes reach the ping endpoint within 60 seconds.

Answers#

StatusMeaning
200 OKThe signal is stored — Belvador answers only after it is safely recorded
404Unknown, revoked or expired URL (the same answer for each, so a URL cannot be guessed)
400A suffix other than start, fail, log or 0–255
403The request came from a web page (Origin header), or the workspace does not accept data right now
405A method other than GET or POST
413The body is larger than 64 KB; nothing is recorded
429More than 60 calls a minute for this monitor (bursts of 20), or the plan's monthly limit. Respect Retry-After
503Try again in a few seconds (Retry-After)

Retrying the same call is safe: send an Idempotency-Key header (or ?rid=<id>) and repeats of one call count once.

Good to know#

  • Never call the URL from a web page. Browsers are refused, and the URL would be visible to every visitor.

  • A ping arriving up to one minute before a cron time counts for that time, to absorb clock drift on the job's host.

  • If Belvador itself was briefly unable to judge, it waits for buffered pings before deciding, and marks a late decision as detected late instead of alerting twice.

Posts on this topic

All posts

Spotted something wrong or missing? Tell us: hello@belvador.dev