Skip to the solution
HardSystem Design7 min read

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

868 views

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.

  • deployment
  • reliability
  • error handling
  • best practices

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Read the code

JavaScriptA 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
});

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 126 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

How do you implement microservices communication in Node.js?

Continue