Skip to main content

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​

TermMeaning
MonitorThe job you expect to hear from, identified by a slug (nightly-backup)
ScheduleA 5-field crontab expression, or an interval (every 30 minutes)
Check-inOne message from the job: in_progress, ok or error
EnvironmentEach environment keeps its own state, so staging can't hide a production miss
Margincheckin_margin minutes of grace after the expected time before it counts as missed
Max runtimemax_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. Default 1.
  • recovery_threshold — how many consecutive successes before it is considered healthy again. Default 1.

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​

SymptomCause / fix
Monitor stuck in pendingNo check-in has ever arrived — verify the slug, project id and key in the URL
Spurious missedThe job runs late; raise checkin_margin, or fix the schedule's timezone
Spurious timeoutmax_runtime is shorter than the job's real runtime
Job runs, monitor errors anywayThe wrapper reports error on a non-zero exit code from something else in the pipeline — check the shell line
Duplicate monitorsTwo spellings of the slug; slugs are normalized, but nightly_backup and nightly-backup are different monitors