← Recipes

阅读中文版本

Add a Graceful Shutdown Handler

Handle SIGTERM in a Node.js service by failing readiness, stopping new connections, draining in-flight requests, and forcing exit after a deadline.

~25 minnodejsoperationsdeployment

Task

Make deployments and restarts stop the process without cutting off active requests or accepting new traffic after the orchestrator begins termination.

When to use this

A container receives SIGTERM during a rolling deploy. Without a shutdown handler, the process exits immediately, clients see reset connections, and readiness keeps reporting healthy until the process disappears.

Prerequisites

  • A Node.js HTTP server or framework with access to the underlying server
  • A readiness endpoint used by the load balancer or orchestrator
  • A termination grace period longer than the drain deadline

Steps

1. Track a shuttingDown flag and make readiness return 503 as soon as termination starts.

let shuttingDown = false;

app.get('/readyz', (_req, res) => {
  res.status(shuttingDown ? 503 : 200).json({ ready: !shuttingDown });
});

Expected output: Once shutdown begins, readiness returns 503 and the load balancer removes the instance.

2. Stop accepting connections, close idle keep-alive sockets, and wait for active requests before exiting.

const server = app.listen(3000);

function shutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  console.log(JSON.stringify({ event: 'shutdown', signal }));

  server.close((error) => {
    if (error) {
      console.error(error);
      process.exitCode = 1;
    }
    process.exit();
  });
  server.closeIdleConnections();

  setTimeout(() => {
    console.error('drain deadline exceeded');
    process.exit(1);
  }, 15_000).unref();
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

Expected output: In-flight requests finish; the process exits 0 after they complete or 1 after the drain deadline.

3. Send SIGTERM during a slow request and confirm the request completes while a new request is refused or redirected.

kill -TERM <pid>

Expected output: The slow request receives a complete response and the process exits within the drain window.

Troubleshooting

Problem

The process waits forever

Solution

Keep-alive connections are still open. Call server.closeIdleConnections and enforce a hard deadline.

Problem

New requests still arrive during shutdown

Solution

Readiness must fail before draining, and the load balancer needs time to observe it.

Limitations

  • Hijacked connections such as WebSockets are not closed by server.close.
  • The deadline must be shorter than the platform's termination grace period.

Shutdown is a sequence

The order matters: fail readiness first, give the load balancer time to stop routing, then close the listener. If the process closes the listener before readiness propagates, some clients still send requests to a socket that is going away.

Keep the handler idempotent. Multiple signals or a signal during a slow shutdown should not start a second drain or reset the deadline. Track the state and log the reason once.

SIGTERM -> readiness 503 -> close listener -> drain active requests -> exit 0
                                      \-> deadline exceeded -> exit 1

Test the drain, not only the exit

A test that sends SIGTERM to an idle server proves very little. Start a request that takes several seconds, send SIGTERM, and verify that the request completes, readiness returns 503 during the drain, and the process exits with the expected code. Also verify the forced-exit path by holding a request longer than the deadline.

Coordinate with the platform

Set the drain deadline below the orchestrator’s grace period, and add a pre-stop delay if the platform does not remove the instance from rotation quickly. Document the values in the deployment manifest so a future change to the grace period does not silently truncate the drain.