Skip to content

Health checks and graceful shutdown ​

Start required resources before listening. On shutdown, stop accepting requests, let active work finish, then close the database. Brevity returns a normal Node HTTP server, so you can use Node's lifecycle methods.

Initialize required resources before listening. This checked example provides separate liveness/readiness routes, rejects readiness while stopping, stops accepting connections, drains existing HTTP work, then closes resources once.

ts
import { createApp } from "@portablesheep/brevity";

type ServiceOptions = {
  port?: number;
  host?: string;
  work: () => Promise<string>;
  closeResources: () => Promise<void>;
  drainTimeoutMs?: number;
};

export function startService(options: ServiceOptions) {
  let ready = true;
  let shutdown: Promise<void> | undefined;
  const app = createApp();
  app.router.get("/health/live", (_req, res) => res.json({ live: true }));
  app.router.get("/health/ready", (_req, res) => res.json({ ready }, ready ? 200 : 503));
  app.router.get("/work", async (_req, res) => res.send(await options.work()));
  const server = app.listen(options.port ?? 3000, options.host ?? "127.0.0.1");
  server.requestTimeout = 30_000;
  server.headersTimeout = 10_000;
  server.keepAliveTimeout = 5_000;
  server.on("request", (_req, res) => {
    res.once("finish", () => {
      if (!ready) setImmediate(() => server.closeIdleConnections());
    });
  });

  function stop(): Promise<void> {
    if (shutdown) return shutdown;
    ready = false;
    shutdown = (async () => {
      let deadlineExceeded = false;
      const deadline = setTimeout(() => {
        deadlineExceeded = true;
        console.error("[service] drain deadline exceeded; closing HTTP connections");
        server.closeAllConnections();
      }, options.drainTimeoutMs ?? 5_000);
      deadline.unref();
      try {
        await new Promise<void>((resolve, reject) => {
          server.close((error) => (error ? reject(error) : resolve()));
          server.closeIdleConnections();
        });
        await options.closeResources();
        if (deadlineExceeded) throw new Error("Service drain deadline exceeded");
      } finally {
        clearTimeout(deadline);
      }
    })();
    return shutdown;
  }

  return { app, server, stop };
}
Signal handling and the process entry point
ts
import { setTimeout } from "node:timers/promises";
import { startService } from "./operations.js";

const service = startService({
  port: 0,
  drainTimeoutMs: Number(process.env.DRAIN_TIMEOUT_MS ?? 5_000),
  async work() {
    console.log("Work started");
    await setTimeout(Number(process.env.WORK_DELAY_MS ?? 100));
    return "Work finished";
  },
  async closeResources() {
    console.log("Resources closed");
  },
});
service.server.on("listening", () => {
  const address = service.server.address();
  if (!address || typeof address === "string") throw new Error("Expected TCP listener");
  console.log(`Listening: http://127.0.0.1:${address.port}`);
});
service.server.on("error", (error) => {
  console.error(error);
  process.exitCode = 1;
});
for (const signal of ["SIGTERM", "SIGINT"] as const) {
  process.once(signal, () => {
    void service.stop().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
  });
}

npm test checks active-request draining, repeated cleanup, SIGTERM, and deadline failure locally. Check your host's termination grace period too.

Before deploying ​

Set the platform's termination grace period longer than your drain deadline. Give database/HTTP calls their own timeouts: Node's requestTimeout limits receiving a request, not executing a handler.

Forced socket closure doesn't cancel async work. Cancel or account for outstanding work before closing resources. A supervisor should enforce a final process-exit deadline; upgraded WebSockets need their own cleanup.

Readiness should reflect the dependencies you need to serve traffic. This sample assumes startup succeeded; it isn't a database health probe.

For HTTPS, proxy trust, assets, durable sessions, backups, and rollout checks, see deployment and the launch checklist.

Small core. Build your way.