---
title: Heartbeat & cron jobs
description: Get alerted when a scheduled job, cron entry or background worker doesn't check in — with exit codes, run duration and captured output.
canonical: https://watchfor.io/docs/monitors/heartbeat
---

# Heartbeat & cron jobs

Get alerted when a scheduled job, cron entry or background worker doesn't check in — with exit codes, run duration and captured output.

Most monitors reach out and test your systems. A **heartbeat monitor** works
the other way around: your job, script or worker pings WatchFor when it runs —
and WatchFor alerts you when the ping **doesn't** arrive on time.

That inversion catches the failures outbound checks can't see: a nightly
backup that silently stopped, a cron entry lost in a server migration, a queue
worker that crashed, a certificate-renewal script that never fired. Nothing to
install — a single HTTP request from wherever the job runs is enough.

## Two schedules, one monitor type

The type picker shows two cards — **Heartbeat** and **Cron job** — that create
the same monitor type with a different expectation:

| Mode | Use it for | You set |
| --- | --- | --- |
| **Heartbeat** (every interval) | Workers and services that check in continuously ("every 5 minutes") | The period (1 minute to 1 year) + a grace period |
| **Cron job** (cron schedule) | Jobs that run at specific times ("03:00 daily") | A standard 5-field cron expression, its timezone (an IANA name such as `Europe/Vilnius`) + a grace period |

The **grace period** is how late a ping may arrive before WatchFor alerts
(0 seconds to 30 days). Set it to absorb normal runtime variance — a backup
that takes 10–40 minutes deserves more grace than a heartbeat that fires every
minute. In heartbeat mode the deadline is a full *period + grace* window
measured from the last ping; in cron mode it is the next scheduled time plus
grace, evaluated in the timezone you chose.

Heartbeat monitors have **no locations** and no check interval — your
systems ping WatchFor, so the multi-location scheduling other types use
doesn't apply. See [Locations & scheduling](/docs/monitors/scheduling).

## The ping URL

Each heartbeat monitor gets a unique ping URL, shown on its **Setup** tab:

```
https://ingest.watchfor.io/p/<your-token>
```

- **Any HTTP method** works — `GET`, `POST`, `HEAD`, whatever your tooling
  sends. A `200 OK` means the ping was recorded.
- The URL token is the only secret — treat it like a password. Anyone with the
  URL can ping the monitor. An unknown token returns `404`.
- The URL becomes active **a few seconds after the monitor is created**. A
  ping fired in the same instant as creation (a script that creates the
  monitor through the API and pings immediately) can get a `404`; wait a
  moment or retry.
- An optional **request body** (up to 10 KB — for example the job's last log
  lines) is stored with the ping and shown in the **Pings** tab.
- Pings are rate-limited per monitor, so a runaway loop can't flood the log.

### Signal endpoints

| URL | Meaning |
| --- | --- |
| `/p/<token>` | Success — the job completed fine |
| `/p/<token>/<exit-code>` | Report the exit code (0–255): `0` = success, anything else = the run **failed** |
| `/p/<token>/fail` | Explicit failure report |
| `/p/<token>/start` | The job just started — pairs with the finish ping to measure run duration |

Add `?rid=<run-id>` (up to 64 characters) to correlate a `/start` with its
finish ping when runs can overlap.

A **failure ping still counts as a check-in** — it tells WatchFor your
scheduling machinery is alive, so you get a *"run failed"* alert instead of a
misleading *"missed schedule"* one.

### Examples

At the end of a cron line — the ping only fires if the job succeeded:

```bash
0 3 * * * /usr/local/bin/backup.sh && curl -fsS https://ingest.watchfor.io/p/<token>
```

Report the real exit code, whatever it is:

```bash
/usr/local/bin/backup.sh; curl -fsS https://ingest.watchfor.io/p/<token>/$?
```

Capture the job's output and attach it to the ping — stored with every ping
(success or failure, up to 10 KB) and shown in the **Pings** tab:

```bash
OUT=$(/usr/local/bin/backup.sh 2>&1)
curl -fsS --data-raw "$OUT" https://ingest.watchfor.io/p/<token>/$?
```

Send a start/finish pair correlated by a run ID — WatchFor measures each
run's duration, shows it per ping and charts average / min / max on the
Overview:

```bash
RID=$(uuidgen)
curl -fsS "https://ingest.watchfor.io/p/<token>/start?rid=$RID"
/usr/local/bin/backup.sh
curl -fsS "https://ingest.watchfor.io/p/<token>/$??rid=$RID"
```

> **Info**
>
> Already using Healthchecks-style pings? The URL conventions
> (`/start`, `/fail`, `/<exit-code>`, `?rid=`) are compatible — migrating is
> usually just swapping the domain in your existing curl lines.

## Alert rules

New heartbeat monitors come with two rules on by default:

| Preset | Default | Fires when |
| --- | --- | --- |
| **Missed Schedule** | on, critical | No ping arrived within the expected window plus grace. One missed window is enough — the grace period already gave the job its slack. |
| **Run Failed** | on, warning | A ping arrived but reported a non-zero exit code or hit `/fail`. |
| **Specific Exit Code** | off | One exact code, e.g. `137` (killed / out of memory) or `124` (timeout). |
| **Run Duration** | off | A run took longer than a threshold (requires start/finish pairs). |

Heartbeats have a single source of truth (the missing ping), so there is no
multi-location confirmation phase — a missed window opens the incident
directly. Incidents, notifications, on-call and status pages then work
exactly like any other monitor type. The alert metrics for the API are
`metrics['heartbeat.missed']`, `metrics['heartbeat.run_failed']`,
`metrics['heartbeat.exit_code']` and `metrics['heartbeat.duration_ms']` — see
[the type catalog](/docs/api/monitor-types#heartbeat--cron).

## Good to know

- **A `/start` ping doesn't count as the check-in** — the finish ping does.
  Resetting the schedule on start would make a job that hangs mid-run look
  healthy.
- A failed *delivery* of your ping (network blip on your side) looks the same
  as a missed run — prefer `curl -fsS --retry 3` in critical jobs.
- Pausing the monitor stops the schedule expectation; pings received while
  paused are ignored.
- **Run check now** isn't available for heartbeats — there is nothing for
  WatchFor to run.
- **Cron expressions are validated when you save**, with the same rules the
  scheduler uses: five fields (minute, hour, day of month, month, day of
  week); `*`, `?`, lists (`1,15`), ranges (`1-5`), steps (`*/5`, `1-30/10`)
  and `JAN`–`DEC` / `SUN`–`SAT` names. Descriptors such as `@daily`, a sixth
  seconds field and `7` for Sunday (use `0`) are rejected with a message
  naming the field — nothing is saved that would silently fall back to an
  hourly schedule. Not sure your cron expression means what you think? Check
  it with the free [cron expression tester](/cron-expression-tester).

## Also via API

Create with `type: "heartbeat"` and `config.scheduleMode` (`period` or
`cron`), `periodSeconds`, `graceSeconds`, `cronExpression` and `timezone`;
the ping token is generated server-side and returned on the monitor — see
[Monitors API](/docs/api/monitors#create-a-monitor) and the
[type catalog](/docs/api/monitor-types#heartbeat--cron).

---

Canonical page: https://watchfor.io/docs/monitors/heartbeat · All docs: https://watchfor.io/docs · Site guide: https://watchfor.io/llms.txt
