The check run
What happens, in order, every time monitors are checked.
One function does the work — statusPage.cron.runChecks(now?) — and every
trigger calls it: the local scheduler, a Cloudflare cron event, a Vercel cron
route, node-cron, heyo-status check run and
POST /admin/checks/run. The order of operations matters, so it is the same
everywhere.
flowchart TD A["runChecks(now)"] --> B["Run every non-composite monitor concurrently"] B --> C["Derive composite monitors from that batch"] C --> D["Count prior consecutive failures (one history read)"] D --> E["Collect monitor.down / monitor.recovered / ssl.expiring"] E --> F["Insert results into status_results"] F --> G["Reconcile automatic incidents"] G --> H["Invalidate the snapshot cache"] H --> I["Dispatch all events to channels and subscribers"]
Step by step
- 1
Run the checks
Every monitor that performs I/O runs concurrently, so a run costs the slowest monitor, not their sum. Each check is wrapped so that it always returns a storable
CheckResult— an exception becomes a failed result with the error message, never a thrown run. - 2
Derive composites
Composite monitors do no I/O. They are evaluated against the results just collected, which is why their components must be monitors that actually perform a check. Results are then reordered to match configuration order, so CLI output and notifications stay stable.
- 3
Count failures before writing
Transition events are decided before the batch is persisted, using one history read for the whole batch. That is what makes
monitor.downfire exactly on the run where the consecutive-failure count reachesnotifications.failureThreshold, andmonitor.recoveredfire exactly once after a threshold-reaching outage. - 4
Persist
Results are inserted into
status_results— one row per monitor per run, with name, type, status, latency, message and timestamp. - 5
Reconcile incidents
With
incidents.enabled, failing monitors can open an automatic incident and recovering ones can progress it toward resolution. See Automatic incidents. - 6
Invalidate and dispatch
The snapshot cache is dropped so the next render is current, then monitor and incident events are delivered to every subscribed channel. Delivery failures are isolated and logged; they never fail the run.
A single result
HTTP checks return extra diagnostics on the in-memory result —
httpStatus, responseHeaders, bodyMatch, jsonMatch — and SSL checks add
expiresInDays and expiring. Those fields drive the event type and the
message; the persisted row keeps the six common columns.
latencyMs is wall-clock time for the whole check, measured with
performance.now() and rounded, so a soft-retried HTTP check honestly
includes its retry delay.
Maintenance suppression
If maintenance.enabled is on, monitors covered by an in-progress window
are filtered out before events are collected and before incidents are
reconciled. They are still checked and still stored — the uptime grid should
show what really happened — they simply do not page anyone.
Running it yourself
The CLI prints one line per monitor and exits non-zero when any check failed, which makes it usable as a smoke test in CI:
heyoStatus() does not start timers. Nothing is checked until a scheduler
calls runChecks, which is what keeps the same configuration safe to import
in a build step, a test or a CLI command.