---
title: "Health and status page"
description: "Serve health probes, drain cleanly on shutdown, watch SLO burn and resource pressure, and register a check for each dependency."
section: "interfaces"
status: "Building for 0.4 · ships as a beta API"
spec: "health"
canonical: "https://mainsdk.dev/docs/interfaces/status-page"
updated: "2026-10-28"
---

# Health and status page

> Serve health probes, drain cleanly on shutdown, watch SLO burn and resource pressure, and register a check for each dependency.

Status: Building for 0.4 · ships as a beta API · Canonical: https://mainsdk.dev/docs/interfaces/status-page · Updated: 2026-10-28

`status` gives your app health probes, a check for each dependency, a clean drain on shutdown and SLO burn alerts. Register a check for each thing you depend on, and MainSDK reports the cached state.

> **Note:** Probes, readiness, graceful shutdown, SLO burn (spec `health`) and resource pressure (spec `resources`) run from the repository today and are not published to npm. The app status page with `status.incident` and routing alerts to people (spec `alerts`) are planned: there is no code for them yet, so this page shows neither.

## At a glance

- Status: Building for 0.4 · ships as a beta API
- Phase: 1 (Core, public beta)
- Milestone: 0.4 Hardening
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/health.md
- Category: Reliability and operations

Calls:

- `status.check(name, fn, { every?, timeout?, critical? })`
- `resources.snapshot()`
- `resources.state()`
- `resources.onPressure(fn)`
- `cgroupCores(read)`
- `cgroupMemory(read)`
- `isServerless(env)`
- `nodeSources(opts)`
- `parseResources(sec)`
- `validateResources(sec)`
- `parseHealth(sec, env)`
- `validateHealth(sec, env, { known })`
- `new Slo(opts)`
- `parseSlo(sec)`
- `validateSlo(sec)`
- `resources(object: snapshot, state, onPressure)`
- `realTimers(const)`

In main.yaml:

```yaml
status:
  driver: builtin
health:
  live: /healthz
  ready: /readyz
  critical: [db]                       # dependencies that make the app unready
  token: secret:HEALTH_TOKEN           # optional: guards the detailed report
  shutdown:
    deadline: 25s
    drain-delay: 5s
slo:
  availability: 99.9%
  exclude: ["GET /healthz"]            # routes left out of the readings
  alert: { to: [log, telemetry] }
resources:
  critical: ["POST /api/checkout/**"]  # never paused or shed
  shed: { loop: 500ms }
  nice: 10                             # 0 to 19
  retry-after: 5s
  max-pause: 5m
```

Guarantees, whatever the driver:

- Health checks per dependency
- Probes read cached state only and never hang on a slow dependency


## Drivers

Pick one in main.yaml with `status.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Built in | `builtin` | yes |  |
| Your status page provider | `provider` |  |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-HEALTH-0005` | check | An `slo` value is invalid: availability outside 90% to 99.999%, or a latency that isn't a duration |
| `MAIN-HEALTH-0006` | check | `health.critical` names a dependency the app doesn't configure, or names `mainsdk-cloud` or `tuf`; or `drain-delay` plus 2s is not less than `deadline` |
| `MAIN-RESOURCE-0003` | check | A `resources` value is invalid: a percentage outside 10% to 100%, a `loop` under 20ms, a `shed` threshold not above its `pressure` threshold, a `critical` pattern that doesn't parse, or `nice` outside 0 to 19 |
| `MAIN-HEALTH-0001` | runtime | This instance is not ready: starting, draining, or a critical dependency is down (503 on `/readyz`) |
| `MAIN-HEALTH-0002` | runtime | A request arrived after the app stopped accepting work (503 with `Retry-After` and `Connection: close`) |
| `MAIN-HEALTH-0003` | runtime | The shutdown deadline cut work that was still running; the record lists what was released |
| `MAIN-HEALTH-0004` | runtime | A probe threw or timed out; its dependency is marked down (logged once per change) |
| `MAIN-HEALTH-0010` | runtime | The availability error budget is burning faster than a burn rule allows |
| `MAIN-HEALTH-0011` | runtime | The latency budget is burning faster than a burn rule allows |
| `MAIN-RESOURCE-0001` | runtime | A new non-critical request or realtime connection was shed (503 with `Retry-After`); logged at most once a minute with a count |
| `MAIN-RESOURCE-0002` | runtime | The instance entered or left `pressure` or `shed`, with the reasons and the values |
| `MAIN-RESOURCE-0004` | runtime | `nice` couldn't be applied; the app starts anyway (logged once) |
| `MAIN-RESOURCE-0005` | runtime | Background work waited longer than `max-pause`; it now runs one at a time until pressure ends |
| `MAIN-RESOURCE-0006` | runtime | The V8 heap limit is above the container's memory limit, so the process can be killed before the heap fills |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| App status page and status.incident (not built; the incident controls are the separate incident interface) | 2 | 0.6 |  |
| Remote dependency state through health() | 1 | 0.4 | `fail-static#dependency-status` |
| health and slo sections in main.yaml | 1 | 0.4 | `health#contract` |
| /healthz and /readyz from every adapter | 1 | 0.4 | `health#probes` |
| Custom readiness probes with status.check | 1 | 0.4 | `health#status-check` |
| Graceful drain on SIGTERM | 1 | 0.4 | `health#drain` |
| Shutdown deadline and release of cut work | 1 | 0.4 | `health#deadline` |
| SLO windows from the request log | 1 | 0.4 | `health#slo-windows` |
| Error-budget burn alerts | 1 | 0.4 | `health#slo-alerts` |
| Probe state and burn in mainsdk info | 1 | 0.4 | `health#info` |
| CPU, event-loop delay and memory sampled against the container's limits | 1 | 0.4 | `resources#sampler` |
| Background work paused and caches trimmed under pressure | 1 | 0.4 | `resources#pause-background` |
| Non-critical requests shed with 503 and Retry-After | 1 | 0.4 | `resources#shed` |
| Burn | 1 | 0.4 | `alerts#sources` |

## MainSDK Cloud

Everything on this page is free in your app.


## Quick start

```yaml title="main.yaml"
schema: 1
health:
  critical: [payments]   # not ready while this is down
```

```ts title="app/status.ts"
import { status } from "mainsdk";

status.check("payments", async () => true, { critical: true });
```

Replace the body with a ping of your provider. The check returns `true` when the dependency is healthy. It runs in the background, never on a request, so a hung dependency never hangs a probe.

## Configuration

```yaml title="main.yaml"
health:
  live: /healthz
  ready: /readyz
  critical: [db]               # dependencies whose "down" makes this instance not ready
  every: 10s
  timeout: 2s
  token: secret:HEALTH_TOKEN   # in production, the detail needs this bearer token
  shutdown:
    signals: [SIGTERM, SIGINT]
    deadline: 25s              # from the signal to exit, everything included
    drain-delay: 5s            # report not ready and keep serving; 0 in dev
slo:
  availability: 99.9%          # share of requests that don't fail with a 5xx
  window: 30d
  exclude: [/healthz, /readyz]
resources:
  every: 1s
  hold: 5s                     # a reading must stay over a threshold this long
  pressure: { cpu: 80%, loop: 100ms, memory: 80% }
  shed: { loop: 500ms }        # off unless you add it
  critical: ["POST /api/checkout/**", "job:billing.*"]
  retry-after: 5s
  max-pause: 5m
```

| Key | What it does |
| --- | --- |
| `health.critical` | Dependencies that make this instance not ready. MainSDK Cloud and TUF can never be listed |
| `health.shutdown.deadline` | The most time from the signal to exit. Work still running is cut, released for retry and reported |
| `slo.availability` | The target. The latency target is `latency_p95` (a duration such as `300ms`, 95% of requests within it); set it in `main.yaml` next to it. Error-budget burn is logged with a code and exported as a metric |
| `resources.pressure` | Thresholds for `pressure`: background jobs and workflow runs wait, and `/readyz` says `degraded` |
| `resources.shed` | Thresholds for `shed`: new non-critical requests get 503 with `Retry-After`. Needs a section; no default |
| `resources.critical` | Routes (`METHOD /path/**`) and `action:`, `job:` or `workflow:` names that still run under pressure and while shedding |
| `resources.max-pause` | Work that waited this long runs one at a time, so it slows down but never starves |

The values shown for `resources` are the defaults, except `shed` and `critical`.

`/healthz` answers 200 while the process can respond. `/readyz` answers 200 with `ready` or `degraded`, and 503 with `starting`, `draining` or `not-ready` plus `MAIN-HEALTH-0001`. It reads cached state. Outside production the body lists every dependency; in production it does so only with the bearer token. On SIGTERM the app reports not ready, keeps serving for `drain-delay`, then stops taking work, drains requests, jobs, workflow steps and realtime streams, and flushes logs and audit. A resource pressure state never makes `/readyz` unready, only `degraded`.

A bad `slo` value fails `mainsdk check` with `MAIN-HEALTH-0005`; a bad `critical` name or a drain delay too close to the deadline fails with `MAIN-HEALTH-0006`; a bad `resources` value fails with `MAIN-RESOURCE-0003`.

## What you get

- `/healthz` and `/readyz` from every server adapter, with no route to write.
- Circuit breaker states from the network layer listed as dependencies.
- Request counting for SLOs: a 5xx is bad, a 4xx is good, under 2 microseconds a request.
- Graceful shutdown in a fixed order inside the deadline, and `mainsdk status` for the current state.

## Examples

### Check a partner API

```ts title="app/status.ts"
import { http, status } from "mainsdk";

status.check("partner-rates", async () => {
  const res = await http.fetch("https://api.partner.com/v1/health");
  return res.ok;
});
```

Allow the host your check calls with [Network permissions](https://mainsdk.dev/docs/interfaces/network). A check that throws, returns `false` or times out marks its dependency `down`. A dependency that is not in `health.critical` shows as `down` in the detail and `/readyz` stays 200.

### See the state of every dependency

`health()` lists the cached state of every probe, circuit breaker, the resource watcher and the disk. It never calls a driver.

```ts title="app/api/health/route.ts"
import { health } from "mainsdk";

export async function GET() {
  const parts = health();
  const down = parts.filter((d) => d.state === "down");
  return Response.json({ ok: down.length === 0, parts }, { status: down.length ? 503 : 200 });
}
```

Each entry has `name`, `state` (`ok`, `degraded` or `down`), `since` (RFC 3339), `critical` and sometimes `detail`. Run `npx mainsdk status` for the same list in your terminal, one line per dependency with its state. Add `--json` for machine-readable output. Registered probes appear once the app has started.

### Trim caches under pressure

```ts title="app/cache.ts"
import { resources } from "mainsdk";

const localCache = new Map<string, string>();   // the app's own in-memory cache
const stop = resources.onPressure(() => localCache.clear());
```

`localCache` is your own. The hook runs when the instance enters `pressure` or `shed`. `resources.state()` returns `ok`, `pressure` or `shed`, `resources.pressure` is `true` while background work waits, and `resources.snapshot()` returns the state, reasons (`cpu`, `loop`, `memory`), `cpu`, `loop` and `memory` readings and `since`. `onPressure` returns a function that removes the hook.

## Reference

Calls beyond the examples above. Every `parse` call reads a section into settings with defaults; every `validate` call returns the issues that `mainsdk check` reports.

| Call | What it does |
| --- | --- |
| `resources(object)` | The resource watcher: `resources.snapshot()`, `resources.state()` and `resources.onPressure(fn)`. |
| `parseResources(sec)` | Reads a `resources` section. Invalid values fall back to the defaults. |
| `validateResources(sec)` | Returns the issues in a `resources` section, each reported as `MAIN-RESOURCE-0003`. |
| `cgroupCores(read)` | Returns the CPU quota in cores from cgroup v2 `cpu.max`, else v1, or `null` when unlimited or absent. `read` returns a file's text or `undefined`. |
| `cgroupMemory(read)` | Returns the container memory limit from cgroup v2 `memory.max`, else v1, or `null` when unlimited or absent. |
| `nodeSources(opts)` | Returns the real readings: Node's counters, V8, the cgroup files and the environment. Takes `{ read?, env? }`. |
| `isServerless(env)` | Returns `true` when the environment names a serverless platform, where an instance freezes between calls. |
| `parseHealth(sec, env)` | Reads a `health` section. The drain delay is 0 in `dev`. |
| `validateHealth(sec, env, { known })` | Returns the issues in a `health` section (`MAIN-HEALTH-0006`): a critical dependency that is MainSDK Cloud or TUF, or one the app does not configure. |
| `new Slo(opts)` | Counts good, bad and slow requests in per-minute buckets and alerts on multi-window error-budget burn. Takes `{ config, log?, now?, timers?, metrics? }`. |
| `parseSlo(sec)` | Reads an `slo` section. A bad value is left out and reported by `check`. |
| `validateSlo(sec)` | Returns the issues in an `slo` section (`MAIN-HEALTH-0005`). |
| `realTimers(const)` | Timers that never keep the process alive. Tests pass a fake clock's timers instead. |

## Errors and checks

Runtime: `MAIN-HEALTH-0001` (not ready, 503 on `/readyz`), `0002` (a request after the app stopped accepting work), `0003` (the deadline cut work), `0004` (a probe threw or timed out), `0010` and `0011` (the availability or latency budget burns too fast). `MAIN-RESOURCE-0001` (a request was shed, 503 with `Retry-After`), `0002` (the state changed), `0004` (`nice` could not be applied), `0005` (background work waited past `max-pause`), `0006` (the heap limit is above the container's). Checks: `MAIN-HEALTH-0005`, `MAIN-HEALTH-0006` and `MAIN-RESOURCE-0003`. See [check codes](https://mainsdk.dev/docs/reference/check-codes) and [Error codes](https://mainsdk.dev/docs/reference/errors).

## Swapping drivers

The `status` driver key (`builtin` or `provider`) belongs to the planned status page. Nothing reads it today: probes, drain, SLOs and resources have no driver.

## Testing

`createTestApp` gives you the app with a testing clock, so probes and the SLO run without waiting. Read the probes from the app's health layer.

```ts title="app/status.test.ts"
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";

it("is not ready while a critical check is down", async () => {
  const app = createTestApp({ yaml: "schema: 1\nproject: prj_test\nhealth:\n  critical: [payments]\n" });
  app.health.check("payments", async () => false, { critical: true });
  await app.health.start();
  const res = app.health.handle(new Request("http://x/readyz"));
  assert.equal(res?.status, 503);
});
```

The test pattern follows `packages/sdk/test/health.test.ts`, which also drives shutdown with fake signals and fake workers. The calls above ran against the repository.

## Security and privacy

- Health checks and resource watching are free in your app.
- In production the detail needs the `health.token` bearer token. Keep check results free of personal data anyway.
- MainSDK's own status page, for MainSDK Cloud and TUF delivery, is separate from yours and hosted apart from the Cloud it reports on.

## FAQ

**Is this the MainSDK status page?** No. `status` is for your app. MainSDK publishes its own page for the Cloud and TUF delivery.

**Does my app go down if MainSDK Cloud does?** No. No MainSDK service is ever in your request path, and MainSDK Cloud can never be a critical dependency. See [Fail static](https://mainsdk.dev/docs/concepts/fail-static).

## Next

- [Watch your app's health and resource pressure](https://mainsdk.dev/docs/guides/alerts-and-health)
- [Fail static](https://mainsdk.dev/docs/concepts/fail-static)
- [Logs, audit and telemetry](https://mainsdk.dev/docs/interfaces/log)
- [Network permissions](https://mainsdk.dev/docs/interfaces/network)
