CronPup

Docs

Two-minute setup

  1. Create an account and add a monitor. Tell it how often the job runs.
  2. Copy the monitor's ping URL.
  3. Request that URL when the job finishes successfully.
*/15 * * * *  /opt/jobs/sync.sh && curl -fsS -m 10 --retry 3 https://cronpup.com/p/UUID

If the ping doesn't arrive within the expected period plus the grace period, the monitor goes down and you're alerted. When pings resume, it goes back up and you get a recovery alert.

Ping API

RequestMeaning
GET|POST|HEAD /p/UUIDSuccess. The job ran.
/p/UUID/startThe job started. Its duration is recorded, and it alerts if no success arrives within the grace period.
/p/UUID/failThe job failed. Alerts immediately.
/p/UUID/EXITCODE0 means success. Any other number means failure.

A POST body (up to 10 KB) is stored with the ping, which is handy for the last lines of output. Responses are 200 OK with body OK, or 404 for an unknown UUID.

Recipes

Step-by-step guides: Kubernetes · systemd timers · Docker · GitHub Actions · AWS Lambda · GitLab CI · Rails · Laravel · Node.js · Go · Python/Django · Celery · Windows · restic/borg

The easiest way: the cronpup-run wrapper

A 30-line POSIX shell script that sends /start, runs your command, then reports its exit code along with the last ~9 KB of output. Your job's own output and exit code pass through unchanged, and network problems never affect the job. Read the script before installing it.

curl -fsSL https://cronpup.com/static/cronpup-run.sh -o /usr/local/bin/cronpup-run && chmod +x /usr/local/bin/cronpup-run

# crontab
0 3 * * *  cronpup-run https://cronpup.com/p/UUID /opt/jobs/backup.sh --full

The cronpup command-line helper

The same wrapper as a small CLI, with a one-line install. It takes the monitor's UUID (or ping-key/slug, or a full ping URL), and also sends single signals. Read the script and the installer first.

curl -fsSL https://cronpup.com/install.sh | sh

# crontab
0 3 * * *  cronpup run UUID -- /opt/jobs/backup.sh --full

# one signal: start, fail, or an exit code
cronpup ping UUID fail

Each monitor's page also has copy-paste setup for systemd, Kubernetes, GitHub Actions, Windows Task Scheduler, Python and Node.

Monitor every job in an existing crontab in one command

cronpup-sync (Python 3, no packages) reads crontab -l and creates one monitor per job, with the same cron expression and timezone. It then rewrites each line to run through cronpup-run. It does a dry run unless you pass --apply, saves a backup first, and skips @reboot and jobs that already ping us.

curl -fsSL https://cronpup.com/static/cronpup-sync.py -o cronpup-sync.py
export CRONPUP_API_KEY=cp_...          # Settings → Create API key
python3 cronpup-sync.py                # review the plan
python3 cronpup-sync.py --apply        # create monitors + update the crontab

Switching from Healthchecks.io

cronpup-import-healthchecks copies your checks (name, period or cron + timezone, grace and tags) using Healthchecks' public API (a read-only key is enough). It does a dry run unless you pass --apply, never changes your Healthchecks account, and writes a CSV mapping each old ping URL to the new one.

HC_API_KEY=... CRONPUP_API_KEY=cp_... python3 cronpup-import-healthchecks.py --apply

Report success or failure from a shell script

#!/bin/sh
URL=https://cronpup.com/p/UUID
curl -fsS -m 10 "$URL/start"
output=$(/opt/jobs/backup.sh 2>&1)
curl -fsS -m 10 --data-raw "$(printf '%s' "$output" | tail -c 9000)" "$URL/$?"

Python

import urllib.request
urllib.request.urlopen("https://cronpup.com/p/UUID", timeout=10)

systemd timer

[Service]
ExecStart=/opt/jobs/report.sh
ExecStartPost=/usr/bin/curl -fsS -m 10 https://cronpup.com/p/UUID

Kubernetes CronJob

command: ["/bin/sh", "-c", "run-report && wget -qO- https://cronpup.com/p/UUID"]

Tuning alerts

SettingWhat it does
Grace periodHow late a ping may be before the monitor goes down. Set it a little above the job's normal run time plus jitter.
Alert after N failuresWith 2, 3 or 5, isolated failure signals (/fail or a non-zero exit code) are recorded but don't alert until that many arrive in a row. A success resets the count. Missed pings always alert after the grace period.
Reminders while downRe-send the down alert every 1, 6 or 24 hours until the monitor recovers. Off by default.
PauseStops alerting and ignores pings until you resume. Resuming waits for the next ping before judging.
Snooze 1h/8h/24hFor maintenance windows: the monitor keeps tracking state, but alerts aren't sent. If it's still down when the snooze ends, you get one alert.
Alert routingBy default a monitor alerts all your channels. On its page, tick specific channels to route only its alerts there, e.g. backups to PagerDuty and reports to Slack.

Organizing many monitors

Give monitors tags (for example prod, backups) in the monitor form or via the API's tags field. Once you have more than 5 monitors, the dashboard shows tag filters and a name search. Select monitors with the checkboxes to pause or resume them together. Clone on a monitor's page copies its settings with a fresh ping URL.

Cron schedules

Pick Cron expression when creating a monitor and paste the same 5-field expression your crontab uses (e.g. 30 2 * * 1-5), along with the timezone the server runs in. Macros such as @daily and @hourly also work. CronPup expects a ping at each scheduled time, plus the grace period.

TLS certificate monitors

Enter a hostname (and a port, if it isn't 443). CronPup connects every 6 hours, validates the certificate chain the way a browser would, and marks the monitor down if it is invalid or expires within your warning window (14 days by default).

Push notifications (ntfy)

Install the free ntfy app (Android, iOS or web), subscribe to a long random topic, and add the same topic under Alerts. Down alerts arrive as high-priority pushes and recoveries as normal ones. Tapping a notification opens the monitor. Self-hosted ntfy servers work too (enter the full https topic URL).

PagerDuty

In PagerDuty, add an Events API v2 integration to a service and copy its Integration Key. Add it under Alerts as a PagerDuty channel. A monitor going down triggers an incident, and recovery resolves the same incident automatically (one incident per monitor).

Webhooks

Add a webhook URL under Alerts. On every status change we POST JSON:

{"text": "nightly-backup is DOWN: ...", "content": "...same...", "monitor": "nightly-backup",
 "status": "down", "reason": "...", "url": "https://cronpup.com/monitors/42", "ts": 1767225600.0,
 "monitor_id": 42, "monitor_uuid": "...", "kind": "heartbeat", "tags": ["prod"], "last_ping": 1767139200.0,
 "schedule": "0 3 * * *"}

text makes it work with Slack incoming webhooks and content with Discord. Webhook targets must be public HTTP(S) URLs, and redirects are not followed.

Verifying webhook signatures

Each webhook channel has a signing secret, shown under Alerts. Every request carries X-CronPup-Timestamp and X-CronPup-Signature: sha256=HEX, where HEX is the HMAC-SHA256 of TIMESTAMP + "." + raw body. Reject requests whose timestamp is more than 5 minutes old.

import hashlib, hmac, time

def valid(secret: str, body: bytes, ts: str, sig: str) -> bool:
    if abs(time.time() - int(ts)) > 300:
        return False
    expected = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Ping by name, and auto-create

Create a ping key on the Settings page. Then any monitor with a slug can be pinged as https://cronpup.com/p/PING_KEY/SLUG, with the same /start, /fail and /EXITCODE suffixes. Add ?create=1 and a slug that doesn't exist yet is created on its first ping. It's created as "every 1 day, 1 hour grace", which you can edit afterwards, and it counts toward your plan limit.

# the same line on every server; each host gets its own monitor
0 4 * * *  /opt/jobs/rotate-logs.sh && curl -fsS -m 10 "https://cronpup.com/p/PING_KEY/rotate-logs-$(hostname -s)?create=1"

Slugs are lowercase letters, digits, - and _. Anyone with the ping key can send pings to (and create) your monitors, so treat it like a password. You can rotate it on the Settings page.

Management API

Create an API key on the Settings page, then send it as a bearer token. All bodies are JSON; times are in seconds.

RequestDoes
GET /api/v1/monitorsList your monitors, including ping URLs and status
POST /api/v1/monitorsCreate a monitor. Fields: name; period (s) or cron+tz; grace (s); optional slug, tags, tolerance (1/2/3/5), remind_h (0/1/6/24); TLS: kind:"tls", host, port, warn_days
GET /api/v1/monitors/IDOne monitor
POST /api/v1/monitors/ID/pause|resume|deletePause, resume or delete it
POST /api/v1/monitors/IDUpdate it: send only the fields to change (same names as create; the kind can't change)
GET /api/v1/metricsPrometheus metrics (see below)
GET /api/v1/monitors/ID/events?limit=100Status transitions (down/up/paused), newest first
GET /api/v1/monitors/ID/pings?limit=100Recent signals with duration and output
curl -H "Authorization: Bearer $TB_KEY" -d '{"name": "nightly-backup", "cron": "0 3 * * *",
     "tz": "Europe/Berlin", "grace": 1800}' https://cronpup.com/api/v1/monitors

curl -H "Authorization: Bearer $TB_KEY" -d '{"name": "queue-worker", "period": 300, "grace": 120}' https://cronpup.com/api/v1/monitors

curl -H "Authorization: Bearer $TB_KEY" -d '{"name": "shop cert", "kind": "tls", "host": "shop.example.com"}' https://cronpup.com/api/v1/monitors

Responses include ping_url, so a provisioning script can create a monitor and write the URL into the job's config.

Prometheus

Scrape https://cronpup.com/api/v1/metrics with your API key to put job health into existing dashboards and alert rules:

scrape_configs:
  - job_name: cronpup
    scheme: https
    metrics_path: /api/v1/metrics
    authorization:
      credentials: cp_YOUR_API_KEY
    static_configs:
      - targets: ["cronpup.com"]

Metrics: cronpup_monitor_up (1/0), cronpup_monitor_last_signal_age_seconds and cronpup_tls_expiry_seconds, labelled with name, id, kind and tags.