Creating and configuring a monitor
A monitor is one thing you want to know about. There are two kinds: a heartbeat, where your job tells PulseWatch it ran, and an uptime check, where PulseWatch fetches a URL. The failure a heartbeat prevents is the one nothing else catches, where cron stops firing a job entirely and there is no error anywhere because nothing ran to produce one.
Which kind you want
| Type | How it works | Use it for |
|---|---|---|
| heartbeat | Your job requests a URL when it runs. Silence past the schedule is the failure. | Cron jobs, backups, ETL, queue workers, anything scheduled. Also anything behind a firewall, since nothing is fetched from outside. |
| uptime | PulseWatch fetches a URL on a schedule and judges the response. | Public endpoints, health checks, certificates. Anything you can reach from the internet. |
Creating a heartbeat monitor
- Sign up and open the dashboard. A Free account can hold 3 monitors, which is enough to start.
- Choose New monitor and then Heartbeat.
- Name it after the job, not after the machine. Alerts are read at 3am by someone deciding whether to get up.
- Set how often it runs. A nightly backup is every 24 hours.
- Set grace. This is the part people get wrong: it should cover the slowest run you would still call normal, not the average one. A backup that usually takes 8 minutes and occasionally takes 25 wants at least 30 minutes of grace, or it will page for being slow.
- Save. The monitor page shows a ping URL. Copy it.
- Add the ping to the job.bash
# The one line to add. --retry 5 so a dropped packet is not an outage, # -m 10 so the ping can never hang the job, -fsS so failures are visible. 0 3 * * * /opt/backup.sh && curl -fsS -m 10 --retry 5 \ https://pulsewatcher.up.railway.app/ping/<token>
- Confirm it worked: run the curl by hand now. The monitor moves from
newtoupwithin a few seconds and the dashboard row reads:If it stays intextNightly backup up every 24h 路 30m grace last ping 03:00:12 (7h ago) next expected by 03:30 tomorrownew, the request did not arrive. Run the curl without-sto see why.
When the job stops running, the alert says what was expected and when it last happened:
馃敶 Nightly backup is down reason: no_ping Expected by 03:30. Last ping was 26h ago, at 03:00 yesterday. https://pulsewatcher.vercel.app/app/monitors/<id>
Creating an uptime monitor
- Choose New monitor and then Uptime.
- Enter the URL. Prefer a health endpoint that checks the database over a homepage that renders from cache, because the homepage will be up during an outage.
- Set the check interval. The floor is 300 seconds on Free and 60 on Pro and Business.
- Leave the expected status range at 200 to 399 unless your endpoint deliberately returns something else.
- On Pro and above, add expected text. A string that only appears when the page is genuinely working turns a status check into a content check, and catches the 200 that renders "database unavailable".
- Confirm it worked: the first check runs within one interval. The monitor page then shows the status code, the latency, and, for an https URL, the certificate expiry date.
Schedules that an interval cannot express
"Every 24 hours" and "at 3am" are different statements. A job on a period is expected 24 hours after its last ping, so a run that drifts an hour late drags the whole schedule with it. A cron schedule expects it at 3am regardless.
Cron also expresses the things intervals cannot: weekdays only, the first of the month, twice a day at fixed times.
0 3 * * 1-5 weekdays at 03:00 0 */4 * * * every four hours, on the hour 0 2 1 * * the first of the month at 02:00 30 6,18 * * * twice a day, 06:30 and 18:30
Set the timezone alongside the expression. A job scheduled in local time on a server that observes daylight saving will otherwise appear to be an hour late twice a year. Cron schedules need Pro or above.
Heartbeat fields
| Field | Type | Default | Range | Effect | Plan |
|---|---|---|---|---|---|
| name | text | (required) | up to 80 characters | What alerts call it. | all |
| schedule_kind | enum | period | period, cron | A fixed interval, or a cron expression for schedules an interval cannot express. | cron needs Pro+ |
| period_seconds | int | (required) | 30 to 31,536,000 | How often the job is expected to ping. | all |
| grace_seconds | int | (required) | 0 to 31,536,000 | How late a ping may be before the monitor goes down. | all |
| cron_expression | text | null | 1 to 100 characters | Standard five-field cron. Required when schedule_kind is cron. | Pro+ |
| cron_timezone | text | null | IANA name | Which clock the expression is read in. Required alongside the expression. | Pro+ |
| max_duration_seconds | int | null | 1 to 86,400 | A run open longer than this goes down. Needs a /start ping. | all |
Uptime fields
| Field | Type | Default | Range | Effect | Plan |
|---|---|---|---|---|---|
| name | text | (required) | up to 80 characters | What alerts call it. | all |
| url | text | (required) | http or https | The URL fetched on each check. | all |
| check_interval_seconds | int | (required) | 60 to 86,400 | How often the URL is fetched. The floor your plan actually allows is 300 on Free and 60 on Pro and Business. | all |
| timeout_ms | int | 10,000 | 1,000 to 60,000 | A slower response counts as a timeout. | all |
| expected_status_min | int | 200 | 100 to 599 | Bottom of the status range treated as healthy. | all |
| expected_status_max | int | 399 | 100 to 599 | Top of that range. Anything outside it is http_error. | all |
| expect_text | text | null | 1 to 200 characters | The body must contain this. Catches a 200 that renders an error page. | Pro+ |
| forbid_text | text | null | 1 to 200 characters | The body must not contain this. | Pro+ |
| degraded_threshold_ms | int | null | 1 to 60,000 | Slower than this and the monitor is flagged degraded while still counting as up. | Pro+ |
Certificate expiry warnings are automatic on Pro and above for https URLs. There is no field: PulseWatch reads the certificate on each check and alerts once inside the warning window, with reason ssl_expiring.
Alerting fields, on both types
| Field | Type | Default | Range | Effect | Plan |
|---|---|---|---|---|---|
| alert_after_seconds | int | 0 | 0 to 86,400 | Wait this long after a failure before sending anything. Absorbs a blip that fixes itself. | Pro+ |
| repeat_interval_seconds | int | 0 (off) | 0, or 900 to 604,800 | Re-send while still down. Acknowledging the incident stops the repeats. | Pro+ |
| alert_on_new_error | boolean | true | Alert the first time a stack-trace fingerprint is seen, even while already down. | Pro+ | |
| escalate_after_consecutive | int | null (off) | 1 to 100 | Alert when the same error fingerprint fails this many runs in a row. | Pro+ |
| warn_delivery | enum | digest_daily | immediate, digest_daily, silent | When warnings reach you. | all |
| warn_escalate_after | int | 3 | 1 to 100 | Consecutive warns after which the monitor goes down with persistent_warnings. | all |
| tags | text[] | none | Filter and group on the dashboard. | all | |
| on_status_page | boolean | false | Publish this monitor on your status page. | Pro+ | |
| runbook_md | text | '' (account default) | up to 20,000 characters | Instructions delivered inside the alert. | all |
| runbook_url | text | null | http or https, up to 500 chars | A link carried on the alert. | all |
Pausing rather than deleting
A paused monitor still accepts pings and still records them. It simply never changes state and never alerts, and the paused period is excluded from the uptime calculation rather than counted as either up or down. Use it while you are working on the job. Deleting throws away the history.
Monitors can also pause themselves. Dropping to a plan with a smaller cap pauses the oldest monitors until the count fits, newest left running, and nothing is deleted. Upgrading resumes exactly those.
Variations
A nightly backup
Heartbeat, 24 hour period, 30 minute grace, a metric floor on rows written. The schedule catches the job that stopped; the floor catches the job that runs and writes nothing.
A job that runs every five minutes
Heartbeat, 300 second period, 120 second grace, alert_after_seconds of 300. One missed run on a five-minute job is usually noise; two in a row is not.
A checkout API
Uptime, 60 second interval, expected text matching a string only present when the database is reachable, degraded threshold at 2000ms. The degraded flag gives you the slowdown before the timeout.
A weekday-only report
Heartbeat on the cron schedule 0 7 * * 1-5. A period-based monitor would report it missing every Saturday.
A long ETL that sometimes hangs
Heartbeat with /start, max_duration_seconds set, and overlap detection on. Catches the run that never finishes, not just the run that never starts.
What can go wrong
Alerts every time the job is slightly slow
Grace is set to the average run rather than the slowest normal one. The pattern is an alert followed by a recovery a few minutes later, repeatedly. The advisor flags it.
The job fails and PulseWatch says nothing
job.sh && curl sends a ping only on success, so a failure looks like silence and waits for the schedule. Report the outcome with /exit/$? instead.
A cron monitor is late by exactly one hour, twice a year
The timezone is UTC while the server runs local time. Set the monitor's timezone to match the machine.
An uptime check passes during an outage
The URL is served from cache or a CDN and never touches the thing that is broken. Point it at a health endpoint that exercises the database, and add expected text.
The check interval will not go below five minutes
That is the Free floor. Pro and Business allow 60 seconds.
Related
- Pinging API for every ping variant and language.
- Fleets when the same job runs on many machines. One fleet instead of 200 monitors.
- How alerting works for what happens once a monitor goes down.
- Exit codes to report how a run ended rather than only that it happened.