Tree-shakable health endpoints for JavaScript servers. A dependency-free core
runs probes against your dependencies and renders ok | degraded | unhealthy; thin adapters mount it as GET /health on your framework; thin
probe packages know how to ping one dependency each.
Runs on Deno, Node ≥ 22, Bun and edge runtimes. Published to JSR and npm. Built and used in production by openstatus, the open-source uptime monitoring and status page platform.
{
"status": "degraded",
"checkedAt": "2026-09-11T12:00:00.000Z",
"latencyMs": 41,
"checks": [
{ "name": "database", "status": "ok", "critical": true, "latencyMs": 3 },
{ "name": "redis", "status": "skipped", "critical": false, "latencyMs": 0 },
{ "name": "tinybird", "status": "timeout", "critical": false, "latencyMs": 5000, "error": "timed out after 5000ms" }
]
}| Package | JSR | npm | Description |
|---|---|---|---|
@openstatus/health |
Probe runner, caching, response rendering, Fetch-API handler |
| Package | JSR | npm | Description |
|---|---|---|---|
@openstatus/health-hono |
Hono adapter | ||
@openstatus/health-elysia |
Elysia adapter | ||
@openstatus/health-express |
Express 4 / 5 adapter | ||
@openstatus/health-next |
Next.js App Router adapter | ||
@openstatus/health-tanstack-start |
TanStack Start adapter |
| Package | JSR | npm | Description |
|---|---|---|---|
@openstatus/health-drizzle |
Drizzle ORM select 1 probe |
||
@openstatus/health-supabase |
Supabase connection-pressure probe | ||
@openstatus/health-tinybird |
Tinybird reachability probe | ||
@openstatus/health-turso |
Turso libSQL select 1 probe (@libsql/client) |
||
@openstatus/health-turso-serverless |
Turso select 1 probe over the serverless driver (@tursodatabase/serverless) |
||
@openstatus/health-unkey |
Unkey liveness probe | ||
@openstatus/health-upstash |
Upstash Redis PING probe over REST |
| Package | JSR | npm | Description |
|---|---|---|---|
@openstatus/health-fly |
Fly.io region, machine and deployment | ||
@openstatus/health-koyeb |
Koyeb region, instance and deployment | ||
@openstatus/health-railway |
Railway region, replica, environment and deployment | ||
@openstatus/health-vercel |
Vercel region, environment and deployment | ||
@openstatus/health-cloudflare |
Cloudflare Workers colo and version metadata |
Each package is its own concern with its own peer dependencies: importing
@openstatus/health-hono never pulls Express, and importing
@openstatus/health-unkey never pulls Drizzle. CI bundles a one-line consumer
of every package and fails if any other framework or client library lands in
the output.
Install the core plus one adapter and the probes you need:
deno add jsr:@openstatus/health jsr:@openstatus/health-hono jsr:@openstatus/health-turso
npm install @openstatus/health @openstatus/health-hono @openstatus/health-tursoEvery adapter exports the same two functions. healthRoute(options) is the
batteries-included form: it mounts GET and HEAD on options.path
(default /health). healthHandler(options) is the primitive underneath — a
plain handler for that framework — for when you want to pick the path, stack
your own middleware in front, or register it the way you register everything
else.
import { Hono } from "hono";
import { healthHandler, healthRoute } from "@openstatus/health-hono";
import { tursoProbe } from "@openstatus/health-turso";
import { unkeyProbe } from "@openstatus/health-unkey";
const app = new Hono();
app.route("/", healthRoute({
probes: [tursoProbe({ client }), unkeyProbe()],
extend: (_report, c) => ({ requestId: c.get("requestId") }),
}));
// or, on a route of your own:
app.on(["GET", "HEAD"], "/health", healthHandler({ probes: [unkeyProbe()] }));import { Elysia } from "elysia";
import { healthRoute } from "@openstatus/health-elysia";
import { tinybirdProbe } from "@openstatus/health-tinybird";
new Elysia().use(healthRoute({ probes: [tinybirdProbe()] })).listen(3000);import express from "express";
import { healthRoute } from "@openstatus/health-express";
import { drizzleProbe } from "@openstatus/health-drizzle";
const app = express();
app.use(healthRoute({ probes: [drizzleProbe({ db })] }));// app/health/route.ts
import { healthRoute } from "@openstatus/health-next";
import { supabaseProbe } from "@openstatus/health-supabase";
// required: keeps Next.js from statically caching the route
export const dynamic = "force-dynamic";
export const { GET, HEAD } = healthRoute({ probes: [supabaseProbe({ client })] });// src/routes/api/health.ts
import { createFileRoute } from "@tanstack/react-router";
import { healthRoute } from "@openstatus/health-tanstack-start";
import { supabaseProbe } from "@openstatus/health-supabase";
export const Route = createFileRoute("/api/health")({
server: { handlers: healthRoute({ probes: [supabaseProbe({ client })] }) },
});import { createHealthHandler } from "@openstatus/health";
Deno.serve(createHealthHandler({ path: "/health", probes: [/* ... */] }));Every entry point takes the same options — probes (or a shared check),
path, cacheMs, staleMs, timeoutMs, deadlineMs, exposeChecks,
unhealthyStatusCode, degradedStatusCode, extend, formatError,
onReport, onError — documented once in
packages/health.
Aggregation: a failing or timed-out critical probe makes the report
unhealthy; a failing non-critical probe makes it degraded; skipped
probes never affect it. Errors are masked as "failed" unless you opt in
with formatError: "message".
Liveness is "the process answers"; readiness is "the process can serve".
Mount the same adapter twice — an empty probe list is always ok:
app.route("/", healthRoute({ path: "/livez", probes: [] }));
app.route("/", healthRoute({ path: "/readyz", probes, deadlineMs: 800, cacheFailuresMs: 0 }));deadlineMs caps the whole round so a hung dependency cannot outlast a
Kubernetes probe's timeoutSeconds; cacheFailuresMs: 0 lets the next poll
see a recovery immediately. Set staleMs to keep answering from the last
report while a refresh runs in the background.
One /health can serve the load balancer and your on-call engineer:
exposeChecks takes a function of the request, and extend output is only
rendered when checks are exposed. If you would rather serve two routes, build
the check once and share it so the probes run once per cache window:
import { createHealthCheck } from "@openstatus/health";
const check = createHealthCheck({ probes, cacheMs: 5000, onReport: log });
app.route("/", healthRoute({ check, exposeChecks: false }));
app.route("/", healthRoute({ check, path: "/_health", extend: flyExtend() }));
// or, one route:
app.route("/", healthRoute({
check,
exposeChecks: (c) => c.req.header("x-health-token") === env.HEALTH_TOKEN,
extend: flyExtend(),
}));check.invalidate() drops the cache — call it after a reconnect or a config
reload.
| Probe | Default name | Critical | Checks |
|---|---|---|---|
tinybirdProbe({ baseUrl? }) |
tinybird |
no | GET {baseUrl}/v0/health |
unkeyProbe({ baseUrl? }) |
unkey |
no | GET {baseUrl}/v2/liveness |
tursoProbe({ client }) |
database |
yes | client.execute("select 1") on a Turso libSQL client |
tursoServerlessProbe({ connection }) |
database |
yes | connection.get("select 1") on a Turso serverless Connection |
drizzleProbe({ db }) |
database |
yes | db.execute(sql\select 1`)ordb.run(...)` |
supabaseProbe({ client, maxConnectionPercent? }) |
supabase |
no | rpc("health_connection_pressure") ≤ threshold |
upstashProbe({ url, token }) |
redis |
no | GET {url}/ping with the REST token |
Every probe factory accepts name, critical, timeoutMs and skip
overrides. Probes take a client instance or a base URL — they never read
process.env themselves.
A probe is a plain object. Resolve for healthy, reject or throw for failed,
and honour the AbortSignal so a timeout actually cancels the work:
import { httpProbe, probe } from "@openstatus/health";
const queue = probe({
name: "queue",
critical: true,
timeoutMs: 1000,
skip: () => !env.QUEUE_URL,
run: async (signal, ctx) => {
const res = await fetch(`${env.QUEUE_URL}/depth`, { signal });
if (!res.ok) throw new Error(`${ctx.name} answered ${res.status}`);
const { depth } = await res.json();
if (depth > 10_000) throw new Error(`queue depth ${depth}`);
},
});
const docs = httpProbe({ name: "docs", url: "https://docs.example.com", method: "HEAD" });skip runs on every request, may be async, and reports the check as
skipped without running it — use it for optional dependencies that are not
configured in every environment. ctx carries the probe's name, critical
flag and effective timeoutMs.
@openstatus/health/testing exports fakeFetch, hangFetch and ready-made
okProbe / failingProbe / hangingProbe fixtures for testing probes and
adapters of your own.
The hosting packages answer a different question from the probes: not "is the
database up" but "which replica is telling me that". Each reads its platform's
own environment — or, on Workers, the request — and renders it under server
through the same extend hook:
import { healthRoute } from "@openstatus/health-hono";
import { flyExtend } from "@openstatus/health-fly";
app.route("/", healthRoute({ probes, extend: flyExtend() }));{
"status": "ok",
"checkedAt": "2026-09-11T12:00:00.000Z",
"latencyMs": 41,
"checks": [{ "name": "database", "status": "ok", "critical": true, "latencyMs": 3 }],
"server": {
"platform": "fly",
"region": "ams",
"instanceId": "148e21ebd47089",
"service": "openstatus-api",
"version": "registry.fly.io/openstatus-api:deployment-01H9RK9EYO9PGNBYAKGXSHV0PH",
"primaryRegion": "cdg"
}
}platform, region, instanceId, service, version and environment mean
the same thing on every platform; anything else is named as that platform names
it. A field is absent rather than guessed when the platform has no equivalent —
Vercel exposes no instance identity, so there is no instanceId there. Values
are passed through exactly as the platform sets them, so region is ams on
Fly and DFW on Cloudflare.
Each package also exports the data on its own — flyServer(), vercelServer()
— so you can compose it with your own fields, or chain platforms if one build
deploys to several. extend may return anything JSON.stringify accepts;
the report's own fields always take precedence over keys of the same name:
extend: (_report, c) => ({
server: flyServer() ?? vercelServer(),
requestId: c.get("requestId"),
}),Off-platform they return undefined and nothing is rendered, so the same build
runs unchanged on your laptop. extend follows exposeChecks: when the checks
are hidden, so is everything extend adds.
deno task check # type-check, lint, fmt, version consistency
deno task test # node:test suites under Deno
deno task build # tsdown -> dist/ for every package
deno task test:node # the same suites under Node against dist/
deno task check:treeshake # no package bundles another framework/client
deno task test-all # all of the aboveSee AGENTS.md for conventions and RELEASING.md
for the release checklist.
openstatus monitors endpoints from regions
around the world and turns the results into status pages and alerts. These
packages are the /health endpoints behind openstatus's own services,
extracted so any JavaScript server can expose one — and so a monitor has
something more useful to poll than 200 OK. Point an
openstatus monitor at the endpoint and assert
on status in the body to be alerted on degraded before it becomes
unhealthy.
Source: github.com/openstatusHQ/health. Issues and PRs welcome.