# Cron monitoring > https://docs.errora.org/en/products/crons 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: ```python from sentry_sdk.crons import monitor @monitor(monitor_slug="nightly-invoices") def nightly_invoices(): ... ``` ```javascript 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: ```php $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: ```bash # 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: ```crontab 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: ```json { "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](/en/products/alerts). ## API ```text 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 |