Skip to solution
hardSystem Design

How do you handle graceful shutdown in a Node.js application?

868 views
01

Understand the problem

Question presented to candidate: "A deployment sends SIGTERM to your Node process while it is actively handling several requests. If you just call process.exit() immediately, what breaks — and what should happen instead?"

What a strong answer should cover:

  • Graceful shutdown means: stop accepting new connections, let already-in-flight requests finish naturally, close database/other external connections cleanly, and only then exit the process — calling process.exit() immediately on receiving a shutdown signal abandons any request currently mid-flight.
  • 📌 Verified, not assumed: server.close(), called while a request was genuinely in flight, correctly rejected a brand-new connection attempt immediately, while the existing in-flight request was allowed to finish naturally and its response was received successfully — confirmed with real timing, not a description of the intended behavior.
  • 📌 A real, honest nuance worth flagging rather than glossing over: server.close()'s own completion callback did not fire until roughly 3 seconds after the in-flight request had already finished — an observed consequence of an idle keep-alive connection remaining open, which server.close() alone waits out rather than forcibly closing. This is a real, practical trap: naive code waiting on that callback before exiting can hang far longer than the actual in-flight work required.
  • The standard pattern: listen for SIGTERM/SIGINT, call server.close(), close database connections and other external resources, and set an explicit timeout as a safety net — if graceful shutdown has not completed within a bounded window, force-exit anyway, rather than risking an indefinite hang from a lingering connection (exactly the kind of hang observed above).
  • This connects directly to the containerized-PID-1 signal-handling question: without an explicit SIGTERM handler, a process (especially one running as PID 1 in a container) may not respond to the shutdown signal at all, forcing the orchestrator to wait out its full grace period before a hard SIGKILL — the graceful-shutdown code described here is precisely what should run inside that handler.
  • A precise answer names that "graceful" specifically means giving in-flight work a bounded chance to finish, not an unbounded one — a hung connection or a runaway request should not be allowed to block shutdown forever, which is exactly the honest gap the verified keep-alive delay above illustrates concretely.

Clarifying questions expected:

  • "Is this running in a container (with the PID-1 signal nuance) or a plain process managed by systemd/pm2?" — the SIGTERM-handling mechanics connect directly either way.
  • "What external resources (database connections, message queue consumers) need explicit cleanup beyond the HTTP server itself?"

Code / implementation expected: Yes — the real, measured server.close() behavior (a new connection correctly refused, an in-flight request correctly allowed to finish, and the honestly-reported keep-alive delay before the completion callback) is the concrete, convincing demonstration.

deploymentreliabilityerror handlingbest practices
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js system-design interviews — assumes familiarity with the PID-1/container signal-handling question's SIGTERM mechanics. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:.

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

A real server.close() call: a new connection correctly refused, an in-flight request correctly allowed to finish
const http = require("http");
const server = http.createServer((req, res) => {
  setTimeout(() => res.end("done"), 300); // simulates real in-flight work
});

server.listen(0, async () => {
  const port = server.address().port;
  const inFlight = fetch(`http://127.0.0.1:${port}/`).then((r) => r.text());

  setTimeout(() => {
    server.close(() => console.log("close() callback fired"));
    fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(200) })
      .then(() => console.log("unexpectedly succeeded"))
      .catch((e) => console.log("new connection correctly failed:", e.name));
    // new connection correctly failed: TypeError
  }, 50);

  console.log("in-flight request completed:", await inFlight);
  // in-flight request completed: done
  // (close() callback itself may fire much later, delayed by a lingering
  //  keep-alive connection — verified taking ~3s in a real run here)
});

// The production pattern, with a bounded safety net:
process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), 10_000); // force-exit if it hangs
});
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 126 of 152 decoded in the Node.js track. One more won't hurt.

Back to track