Docs
Two-minute setup
- Create an account and add a monitor. Tell it how often the job runs.
- Copy the monitor's ping URL.
- 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
| Request | Meaning |
|---|---|
GET|POST|HEAD /p/UUID | Success. The job ran. |
/p/UUID/start | The job started. Its duration is recorded, and it alerts if no success arrives within the grace period. |
/p/UUID/fail | The job failed. Alerts immediately. |
/p/UUID/EXITCODE | 0 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
| Setting | What it does |
|---|---|
| Grace period | How 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 failures | With 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 down | Re-send the down alert every 1, 6 or 24 hours until the monitor recovers. Off by default. |
| Pause | Stops alerting and ignores pings until you resume. Resuming waits for the next ping before judging. |
| Snooze 1h/8h/24h | For 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 routing | By 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.
| Request | Does |
|---|---|
GET /api/v1/monitors | List your monitors, including ping URLs and status |
POST /api/v1/monitors | Create 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/ID | One monitor |
POST /api/v1/monitors/ID/pause|resume|delete | Pause, resume or delete it |
POST /api/v1/monitors/ID | Update it: send only the fields to change (same names as create; the kind can't change) |
GET /api/v1/metrics | Prometheus metrics (see below) |
GET /api/v1/monitors/ID/events?limit=100 | Status transitions (down/up/paused), newest first |
GET /api/v1/monitors/ID/pings?limit=100 | Recent 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.