| name | health-checks |
| description | Built-in liveness (`/health`) and readiness (`/ready`) endpoints plus graceful HTTP request draining for zero-downtime deploys — the `health` registry (`health.addCheck`/`removeCheck`), the `http.health.*` and `http.gracefulShutdown.*` config, and how readiness ties into `Application.isShuttingDown`. Triggers: `health`, `health.addCheck`, `health.removeCheck`, `HealthCheck`, `/health`, `/ready`, `http.health`, `http.gracefulShutdown`, `forceCloseConnections`, "liveness probe", "readiness probe", "graceful shutdown", "drain in-flight requests", "zero-downtime deploy", "kubernetes health check", "503 until ready"; typical import `import { health } from "@warlock.js/core"`. Skip: the `Application.onShutdown` / `onceBooted` lifecycle hooks — `@warlock.js/core/use-app-context/SKILL.md`; maintenance-mode 503s — `@warlock.js/core/use-middleware/SKILL.md`; connector lifecycle — `@warlock.js/core/add-connector/SKILL.md`; competing libs `@fastify/under-pressure`, `terminus`, hand-rolled `/health` controllers. |
Warlock — health checks & graceful shutdown
Two endpoints and a drain, so a load balancer never routes to an instance that isn't ready and a deploy never kills an in-flight request. All built in; no controller to hand-roll.
The two endpoints
The HTTP connector registers them on the Fastify server during boot (before route scanning), so they exist by the time the server listens:
| Path | Probe | 200 when | 503 when |
|---|
/health | liveness | the process is up | shutdown has begun |
/ready | readiness | booted and not shutting down and every check passes | before boot, during shutdown, or any failing check |
Liveness answers "should the orchestrator RESTART me?" — it ignores dependency checks (a failing DB doesn't mean restart the pod). Readiness answers "should the load balancer ROUTE to me?" — it gates on boot completion, shutdown state, and your registered checks.
GET /health → 200 {"status":"ok"}
GET /ready → 200 {"status":"ok","checks":{"db":true}}
→ 503 {"status":"error","checks":{"db":false}}
Config
const httpConfigurations: HttpConfigurations = {
health: {
enabled: true,
path: "/health",
readinessPath: "/ready",
},
};
Readiness checks
Readiness is isBooted && !isShuttingDown plus every registered check. Register a check from a connector, a main.ts, or anywhere:
import { health } from "@warlock.js/core";
health.addCheck("db", async () => {
return database.isConnected();
});
health.removeCheck("db");
A check returns boolean | Promise<boolean>. A thrown error counts as a failed check (it's surfaced in the checks map + the 503, not logged — probes poll often, so a failure is a normal signal, not an error event). Keep checks cheap and fast; they run on every /ready poll.
Routes-registered readiness signal
A booted HTTP app that ends up with zero routes almost always means a route module failed to register silently — without this check it would 404 every request while reporting ready. health.addRoutesRegisteredCheck(getRouteCount) registers a "routes" check that fails while the count is 0:
import { health, router } from "@warlock.js/core";
health.addRoutesRegisteredCheck(() => router.routeCount());
The count is passed as a getter so the registry stays decoupled from the router. Apps with no HTTP surface simply never register it.
Graceful shutdown (request draining)
On SIGINT/SIGTERM the framework tears down in order: app onShutdown hooks → connectors in reverse priority. The HTTP connector's teardown drains instead of dropping:
Application.isShuttingDown flips true at the very start → /ready immediately returns 503, so the load balancer stops sending new traffic.
- Fastify stops accepting new requests (answers 503 while closing) and lets in-flight ones finish.
- Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
const httpConfigurations: HttpConfigurations = {
gracefulShutdown: {
timeout: 10_000,
forceCloseConnections: "idle",
},
};
forceCloseConnections: "idle" (default) closes idle keep-alive connections and lets active requests finish; true force-closes everything immediately; false waits for every connection.
The zero-downtime deploy flow
SIGTERM → isShuttingDown = true → /ready returns 503
→ LB stops routing new requests to this instance
→ in-flight requests drain (up to gracefulShutdown.timeout)
→ app onShutdown hooks already ran (db/cache still up)
→ connectors close in reverse → process exits
For an even smoother handoff, give the load balancer time to observe the 503 before the server closes — e.g. an onShutdown hook with a short sleep matched to your LB's health-check interval.
Gotchas
/health is registered straight on Fastify, not the app router. It's infra, so it's immune to HMR and route scanning — but if your app also defines a /health route you'll have a collision. Rename via http.health.path.
- Readiness needs a finished boot. Before
Application.isBooted (e.g. while late-phase connectors are still starting) /ready is 503 by design — that's the point.
- A hanging
onShutdown hook delays the drain. App hooks run before connector teardown and are only bounded by your process manager's kill timeout; keep them fast. The HTTP drain itself is bounded by gracefulShutdown.timeout.
- The
maintenance middleware is a different 503. It allowlists /health by default so probes pass during maintenance — but maintenance mode is operator-toggled downtime, not readiness. See @warlock.js/core/use-middleware/SKILL.md.
See also