Monitoring Cron Jobs With One curl

Published: April 12th 2026
Tags: heartbeats, cron, devops, monitoring


Your backup cron job ran successfully last Tuesday. You're fairly sure it ran Wednesday. Thursday? You'd have to check the logs — assuming they weren't rotated.

This is the silent failure problem. Cron jobs don't page you when they stop running. They just don't run, quietly, indefinitely, until you notice the consequences.

The gap in traditional monitoring

HTTP monitors check if a URL responds. They're great for catching when a web service goes down. But they can't tell you if a background job ran — a healthy health endpoint gives you a 200 even if the nightly backup crashed at 3am.

The only reliable way to know a cron job ran is if the cron job tells you it ran.

The dead-man's switch pattern

This is a well-known pattern in reliability engineering: you configure a system to alert if it doesn't hear from a process by a deadline. No heartbeat = something went wrong.

Healthchecks.io popularized this for cron jobs. The pattern is:

  1. Create a "heartbeat" monitor with an expected check-in interval
  2. Add a ping URL call at the end of your cron job
  3. If no ping arrives within the interval + grace period, an incident opens

I just recently coded this pattern into UpWatch

How it works

Here's the before and after for a nightly database backup:

Before:

0 3 * * * /opt/scripts/backup.sh

After:

0 3 * * * /opt/scripts/backup.sh && \
  curl -fsS https://upwatch.dev/ping/YOUR_TOKEN > /dev/null

The && means the curl only fires if the backup script exits 0. If the backup fails, no ping fires. If no ping fires and enough time passes, Upwatch opens an incident and sends you an alert.

That's it. You're now monitoring your cron job!

Setting up a heartbeat

In the Upwatch dashboard, go to HeartbeatsNew Heartbeat.

  • Name: Something descriptive ("Nightly DB backup", "Weekly report", "Cert renewal")
  • Expected interval: How often the job runs (5 minutes to 7 days)
  • Grace period: Buffer beyond the interval before alerting (absorbs natural jitter from server load)

After creating the heartbeat, you get a unique ping URL. Copy it and append the curl call to your cron job.

The /fail endpoint

Sometimes your script knows it failed before the silence timeout would trigger. For those cases, use the explicit fail endpoint:

#!/bin/bash
/opt/scripts/backup.sh
if [ $? -ne 0 ]; then
  curl -fsS -X POST https://upwatch.dev/ping/TOKEN/fail
  exit 1
fi
curl -fsS https://upwatch.dev/ping/TOKEN

This triggers an incident immediately, without waiting for the grace period to expire.

Works everywhere

The ping endpoint accepts GET, POST, and HEAD on the success path, so it works from any runtime:

# Python
import urllib.request
urllib.request.urlopen("https://upwatch.dev/ping/TOKEN", timeout=10)
# GitHub Actions
- name: Ping heartbeat
  if: success()
  run: curl -fsS https://upwatch.dev/ping/${{ secrets.UPWATCH_HB_TOKEN }}
# wget
wget -q -O /dev/null https://upwatch.dev/ping/TOKEN

What to monitor

People use heartbeats for:

  • Database backups — pg_dump, mysqldump, MongoDB Atlas snapshots
  • ETL pipelines — data sync jobs, report generators, warehouse loads
  • Certificate renewals — certbot, acme.sh, any cert automation
  • Cleanup scripts — temp file cleanup, log rotation, cache prune
  • CI / deploy jobs — scheduled test runs, deployment pipelines
  • Email dispatchers — the weekly digest, invoice mailer, scheduled reports

The rule of thumb: anything that runs on a schedule and would cause a problem if it silently stopped.

Free tier

The free tier includes 10 heartbeats — separate from your 10 outbound monitors, so adding heartbeat monitoring doesn't cut into your website monitoring quota. Pro gets unlimited.


Set up your first heartbeat →

Full documentation →