Heartbeat monitoring for cron jobs
Detect when your nightly job stops running. Push, don't poll.
Heartbeat monitoring for cron jobs
A heartbeat monitor flips the model: instead of Happy Uptime calling out, your job calls in. If the call doesn't arrive within the expected window, the monitor goes down and alerts fire.
When to use it
- Cron jobs that should run every N minutes/hours/days.
- ETL pipelines.
- Background workers that should be processing items.
- Anything internal that Happy Uptime's edge can't reach.
Setup
Create the monitor
Pick type: heartbeat. Set:
heartbeat_url— a unique slug, e.g.nightly-etl-prod. This becomes your ping URL.heartbeat_interval(seconds) — how often you expect the job to ping.heartbeat_grace(seconds) — how long after a missed ping before goingdown. Cover normal job duration.
bashhappy monitors create \ --type heartbeat \ --name "Nightly ETL" \ --heartbeat-url nightly-etl-prod \ --heartbeat-interval 86400 \ --heartbeat-grace 1800
Ping from your job
At the end of every successful run, POST to:
texthttps://happyuptime.com/api/heartbeat/{your-heartbeat-url}
No auth required (the slug is the secret).
bash# Last line of your cron scriptcurl -fsS -X POST https://happyuptime.com/api/heartbeat/nightly-etl-prod
Or with a status payload:
bashcurl -X POST https://happyuptime.com/api/heartbeat/nightly-etl-prod \ -H "Content-Type: application/json" \ -d '{ "status": "ok", "message": "Processed 1247 records" }'
Send "status": "fail" when the job ran but failed — the monitor goes down immediately and the message shows in the alert.
Verify with a test ping
Run the curl manually. Check the dashboard within 30s — you should see a green up status and the message.
Patterns
Append the curl to the cron line:
cron0 2 * * * /usr/local/bin/run-etl && curl -fsS -X POST https://happyuptime.com/api/heartbeat/nightly-etl-prod
&& ensures the ping only fires on success. Use ; curl ... to ping regardless of exit code.
jsasync function main() { try { await runJob(); await fetch(`https://happyuptime.com/api/heartbeat/${process.env.HEARTBEAT_SLUG}`, { method: "POST", body: JSON.stringify({ status: "ok" }), headers: { "Content-Type": "application/json" }, }); } catch (err) { await fetch(`https://happyuptime.com/api/heartbeat/${process.env.HEARTBEAT_SLUG}`, { method: "POST", body: JSON.stringify({ status: "fail", message: String(err) }), headers: { "Content-Type": "application/json" }, }); process.exit(1); }}main();
yaml- name: Run job run: ./script.sh id: job- name: Ping heartbeat (success) if: success() run: curl -fsS -X POST https://happyuptime.com/api/heartbeat/${{ secrets.HB_SLUG }}- name: Ping heartbeat (failure) if: failure() run: | curl -X POST https://happyuptime.com/api/heartbeat/${{ secrets.HB_SLUG }} \ -H "Content-Type: application/json" \ -d '{"status":"fail","message":"GitHub Actions job failed"}'
How "down" is detected
The cron scheduler checks every minute: for any heartbeat monitor whose last_checked_at is older than heartbeat_interval + heartbeat_grace, mark it down. Same alert pipeline as any other monitor — Slack page, escalation, status page incident.
Choosing the interval and grace
| Job runs every | Recommended heartbeat_interval | Recommended heartbeat_grace |
|---|---|---|
| Hour | 3600 | 600 (10 min) |
| Day | 86400 | 1800 (30 min) |
| Week | 604800 | 7200 (2 hours) |
Grace should comfortably cover the longest the job has ever taken. If your daily ETL usually finishes in 15 minutes but occasionally takes 45, set grace to at least 60 minutes.