Recipe · scheduled jobs

The backup that proves it ran.

Nobody notices a broken backup until restore day. Give the job a heartbeat: if the ping does not arrive on time, the silence becomes an incident — with the same alerts as everything else.

1 · Create the heartbeat

In the app (Scheduled jobs → New) or via API:

curl -s -X POST https://app.upfour.io/v1/heartbeats \
  -H "Authorization: Bearer $UP4_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "nightly pg_dump",
    "expected_interval_seconds": 86400,
    "grace_period_seconds": 3600
  }'

The response carries the slug — that slug is the ping URL.

2 · Size the grace window

The job runs daily (86400), but not to the second: the dump takes 20–40 minutes depending on load. The grace window absorbs that spread — interval = the schedule, grace = the jitter. One hour of grace on a daily backup means: alert only when the ping is more than an hour late. Too tight pages you for a slow night; too loose costs you reaction time.

3 · The crontab line

# m h dom mon dow  command
30 3 * * *  pg_dump mydb | gzip > /backups/nightly.sql.gz \
              && curl -fsS --max-time 10 --retry 3 \
                 https://app.upfour.io/v1/heartbeat/<your-slug>

Three details carry the whole recipe:

&&, never ;The ping must depend on the dump's exit code. With ; a failed backup still pings — the heartbeat would lie.
-fcurl exits non-zero on HTTP errors instead of swallowing them.
--max-time 10 --retry 3A hanging ping must not block cron; a transient network blip should not look like a missed backup.

4 · What happens when it breaks

Backup fails → no ping → interval + grace passes → incident opens, your alert plan fires, heartbeat.missed goes out as a webhook. The next successful ping recovers it and sends the all-clear. Every ping is logged with its timestamp and source IP — GET /heartbeats/{id}/pings is your audit trail for “did it really run on the 14th?”

Variants

Weekly jobs: 604800 with a day of grace. Multiple backup hosts: one heartbeat each — a shared heartbeat can't tell you which host went quiet. Migration downtime: pause the heartbeat, resume after; paused heartbeats never alert.