Node.js cookbook · Beginner

Add health and readiness endpoints to a Node.js service

Let Kubernetes restart stuck processes and route traffic only to instances that can serve it.

Approach

/healthz checks only that the event loop responds (optionally that event-loop lag is sane). /readyz pings critical dependencies with timeouts and returns 503 when not ready or shutting down. Keep them unauthenticated, cheap, and out of request logs.

Solution

import { monitorEventLoopDelay } from "node:perf_hooks";

const loopDelay = monitorEventLoopDelay({ resolution: 20 });
loopDelay.enable();

const withTimeout = <T>(p: Promise<T>, ms: number) =>
  Promise.race([p, new Promise<never>((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))]);

app.get("/healthz", (_req, res) => {
  const p99ms = loopDelay.percentile(99) / 1e6;
  loopDelay.reset();
  res.status(p99ms < 1000 ? 200 : 503).json({ status: "ok", eventLoopP99Ms: Math.round(p99ms) });
});

app.get("/readyz", async (_req, res) => {
  if (shuttingDown) return res.status(503).json({ status: "shutting_down" });
  const checks = await Promise.allSettled([
    withTimeout(pool.query("SELECT 1"), 1000),
    withTimeout(redis.ping(), 1000),
  ]);
  const [db, cache] = checks.map((c) => (c.status === "fulfilled" ? "ok" : "fail"));
  const ready = db === "ok" && cache === "ok";
  res.status(ready ? 200 : 503).json({ status: ready ? "ready" : "not_ready", checks: { db, cache } });
});

Pitfalls

  • Liveness depending on the database.
  • Probes without timeouts.
  • Heavy work in probes running every few seconds.

Variations

  • @godaddy/terminus bundles health checks with graceful shutdown.

Further reading: Kubernetes: Probes

Reference: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/

More Node.js recipes