Health and status page
statusServe health probes, drain cleanly on shutdown, watch SLO burn and resource pressure, and register a check for each dependency.
healthFree in your app, Apache-2.0Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
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.
At a glance
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: 5mimport { status } from "mainsdk";
status.check("payments", async () => (await pingProvider()).ok);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);
});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)
Drivers
2 drivers. Select one to see its line in main.yaml.
status:
driver: builtinChange this one line to swap providers; your calls stay the same.Built-in and local drivers integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-HEALTH-0005 | Checkmainsdk check, before deploy | An slo value is invalid: availability outside 90% to 99.999%, or a latency that isn't a duration |
MAIN-HEALTH-0006 | Checkmainsdk check, before deploy | 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 | Checkmainsdk check, before deploy | 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 | RuntimeError responses and logs | This instance is not ready: starting, draining, or a critical dependency is down (503 on /readyz) |
MAIN-HEALTH-0002 | RuntimeError responses and logs | A request arrived after the app stopped accepting work (503 with Retry-After and Connection: close) |
MAIN-HEALTH-0003 | RuntimeError responses and logs | The shutdown deadline cut work that was still running; the record lists what was released |
MAIN-HEALTH-0004 | RuntimeError responses and logs | A probe threw or timed out; its dependency is marked down (logged once per change) |
MAIN-HEALTH-0010 | RuntimeError responses and logs | The availability error budget is burning faster than a burn rule allows |
MAIN-HEALTH-0011 | RuntimeError responses and logs | The latency budget is burning faster than a burn rule allows |
MAIN-RESOURCE-0001 | RuntimeError responses and logs | 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 | RuntimeError responses and logs | The instance entered or left pressure or shed, with the reasons and the values |
MAIN-RESOURCE-0004 | RuntimeError responses and logs | nice couldn't be applied; the app starts anyway (logged once) |
MAIN-RESOURCE-0005 | RuntimeError responses and logs | Background work waited longer than max-pause; it now runs one at a time until pressure ends |
MAIN-RESOURCE-0006 | RuntimeError responses and logs | The V8 heap limit is above the container's memory limit, so the process can be killed before the heap fills |
Features
Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| App status page and status.incident (not built; the incident controls are the separate incident interface) | Phase 2 | 0.6 | Not specified yet |
| Remote dependency state through health() | Phase 1 | 0.4 | fail-static#dependency-status |
| health and slo sections in main.yaml | Phase 1 | 0.4 | health#contract |
| /healthz and /readyz from every adapter | Phase 1 | 0.4 | health#probes |
| Custom readiness probes with status.check | Phase 1 | 0.4 | health#status-check |
| Graceful drain on SIGTERM | Phase 1 | 0.4 | health#drain |
| Shutdown deadline and release of cut work | Phase 1 | 0.4 | health#deadline |
| SLO windows from the request log | Phase 1 | 0.4 | health#slo-windows |
| Error-budget burn alerts | Phase 1 | 0.4 | health#slo-alerts |
| Probe state and burn in mainsdk info | Phase 1 | 0.4 | health#info |
| CPU, event-loop delay and memory sampled against the container's limits | Phase 1 | 0.4 | resources#sampler |
| Background work paused and caches trimmed under pressure | Phase 1 | 0.4 | resources#pause-background |
| Non-critical requests shed with 503 and Retry-After | Phase 1 | 0.4 | resources#shed |
| Burn | Phase 1 | 0.4 | alerts#sources |
Quick start
schema: 1
health:
critical: [payments] # not ready while this is downimport { 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
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
/healthzand/readyzfrom 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 statusfor the current state.
Examples
Check a partner API
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. 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.
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
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 and Error codes.
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.
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.tokenbearer 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.
Next
Was this page helpful?