Cron monitoring
A cron monitor watches a scheduled job and complains when it errors, never ran
(missed check-in) or ran too long (timeout). Check-ins come from any Sentry SDK, or from
a plain curl in a crontab line.
Concepts
| Term | Meaning |
|---|---|
| Monitor | The job you expect to hear from, identified by a slug (nightly-backup) |
| Schedule | A 5-field crontab expression, or an interval (every 30 minutes) |
| Check-in | One message from the job: in_progress, ok or error |
| Environment | Each environment keeps its own state, so staging can't hide a production miss |
| Margin | checkin_margin minutes of grace after the expected time before it counts as missed |
| Max runtime | max_runtime minutes after an in_progress check-in before it counts as timed out |
Setup — option 1: create the monitor from the job
Sentry SDKs can create/update the monitor with their first check-in, so the schedule lives next to the code:
from sentry_sdk.crons import monitor
@monitor(monitor_slug="nightly-invoices")
def nightly_invoices():
...
const checkInId = Sentry.captureCheckIn(
{ monitorSlug: "nightly-invoices", status: "in_progress" },
{
schedule: { type: "crontab", value: "0 3 * * *" },
checkinMargin: 5,
maxRuntime: 30,
timezone: "Asia/Tehran",
},
);
try {
await run();
Sentry.captureCheckIn({ checkInId, monitorSlug: "nightly-invoices", status: "ok" });
} catch (e) {
Sentry.captureCheckIn({ checkInId, monitorSlug: "nightly-invoices", status: "error" });
throw e;
}
Laravel:
$schedule->command('invoices:generate')->dailyAt('03:00')->sentryMonitor('nightly-invoices');
Setup — option 2: plain HTTP (no SDK)
Any job that can run curl can check in. The DSN public key in the path authenticates it:
# Simple: one call when the job finishes
curl -X POST "$ERRORA/api/$PROJECT_ID/cron/nightly-backup/$PUBLIC_KEY/?status=ok"
# Better: bracket the run so duration and timeouts are tracked
CHECK_IN=$(curl -s -X POST "$ERRORA/api/$PROJECT_ID/cron/nightly-backup/$PUBLIC_KEY/?status=in_progress" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
if /usr/local/bin/backup.sh; then
curl -X PUT "$ERRORA/api/$PROJECT_ID/cron/nightly-backup/$PUBLIC_KEY/$CHECK_IN/?status=ok"
else
curl -X PUT "$ERRORA/api/$PROJECT_ID/cron/nightly-backup/$PUBLIC_KEY/$CHECK_IN/?status=error"
fi
In a crontab line, the one-shot form is usually enough:
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -X POST "https://errora.example.com/api/$PROJECT_ID/cron/nightly-backup/$KEY/?status=ok"
You can also pass environment= and duration= (milliseconds) as query parameters.
Setup — option 3: create the monitor in the UI
Crons → New monitor. You get the same fields the API takes:
{
"slug": "nightly-invoices",
"name": "Nightly invoices",
"config": {
"schedule_type": "crontab",
"schedule": "0 3 * * *",
"checkin_margin": 5,
"max_runtime": 30,
"timezone": "Asia/Tehran",
"failure_issue_threshold": 1,
"recovery_threshold": 1
}
}
Interval schedules use "schedule_type": "interval" with "schedule": [30, "minute"].
Units: minute, hour, day, week, month, year.
Crontab macros work too: @hourly, @daily, @midnight, @weekly, @monthly,
@yearly. Everything is evaluated in the monitor's own timezone, so a 0 3 * * *
monitor in Asia/Tehran stays at 03:00 local across DST changes.
Thresholds — don't page on one blip
failure_issue_threshold— how many consecutive failures (error / missed / timeout) before an issue is opened and alerts fire. Default1.recovery_threshold— how many consecutive successes before it is considered healthy again. Default1.
Set the failure threshold to 2 or 3 for jobs that are allowed to flap.
In the UI
Crons lists monitors with their schedule, next expected check-in, current status per environment, and a check-in history. Open one for the recent check-ins (status, duration, release, trace id) and its statistics.
Statuses: ok, error, missed, timeout, in_progress, plus pending before the first
check-in ever arrives.
Alerts
Cron state changes fire monitor.failed and monitor.recovered. Wire them to a channel in
Settings → Webhooks and an alert rule — see Alerts.
API
GET /api/v1/projects/{project_id}/monitors
POST /api/v1/projects/{project_id}/monitors
GET /api/v1/projects/{project_id}/monitors/overview
GET /api/v1/projects/{project_id}/monitors/{slug}
PATCH /api/v1/projects/{project_id}/monitors/{slug}
DELETE /api/v1/projects/{project_id}/monitors/{slug}
GET /api/v1/projects/{project_id}/monitors/{slug}/checkins
GET /api/v1/projects/{project_id}/monitors/{slug}/stats
Monitors are addressable by slug as well as id, because the job code knows the slug.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
Monitor stuck in pending | No check-in has ever arrived — verify the slug, project id and key in the URL |
Spurious missed | The job runs late; raise checkin_margin, or fix the schedule's timezone |
Spurious timeout | max_runtime is shorter than the job's real runtime |
| Job runs, monitor errors anyway | The wrapper reports error on a non-zero exit code from something else in the pipeline — check the shell line |
| Duplicate monitors | Two spellings of the slug; slugs are normalized, but nightly_backup and nightly-backup are different monitors |