Graceful Node.js shutdown in production

A reliable shutdown sequence for HTTP servers, load balancers, database pools, background work, health checks, and container timeouts.

Node.jsExpressDeploymentReliability
Node.js server draining active requests during a graceful shutdown

A Node.js process should not exit the moment it receives SIGTERM. During a deployment, the instance may still have active requests, open database work, and a load balancer that has not finished removing it from service.

Stop accepting new work first

Mark readiness as false, then call the HTTP server's close method so new connections are refused while existing requests have a chance to finish. Keep liveness separate from readiness so the platform does not kill a process that is draining correctly.

Readiness before connection draining

The platform needs time to observe that the instance is no longer ready and remove it from service discovery or the load balancer. A short configured drain delay can cover that propagation window before closing listeners, but it should be measured rather than guessed. If the proxy keeps persistent connections, verify how it reacts when the upstream begins draining.

server.close() stops accepting new connections through that server and waits for active ones, but shutdown behavior varies with the Node.js version and connection type. Track open HTTP, keep-alive and upgraded connections explicitly where the framework does not provide the control needed. Long-lived WebSocket or server-sent event clients need a reconnect message or bounded close policy.

Close dependencies in a defined order

Pause queue consumers, wait for bounded in-flight work, close database and cache pools, and flush essential telemetry. Every step needs a timeout because a dependency failure should not leave the deployment hanging indefinitely.

  1. Reject new readiness checks and stop accepting new scheduled or queue work.
  2. Stop the HTTP listener from taking new connections.
  3. Wait for active requests and jobs up to a measured deadline.
  4. Commit, release or return in-flight work according to its queue semantics.
  5. Close database, cache, message-broker and external-client pools.
  6. Flush bounded logs and tracing, then exit with an accurate status.

The order is application-specific. A worker that closes its database pool before its final job commit creates retries or data loss. An HTTP service that closes telemetry before draining requests removes the evidence needed to diagnose slow shutdown.

Make shutdown idempotent

Signals can arrive more than once. Protect the shutdown function with a shared promise or state flag, log the phase once, and set the final exit code based on whether draining completed cleanly.

let shutdownPromise;

function shutdown(signal) {
  if (shutdownPromise) return shutdownPromise;
  shutdownPromise = runShutdown(signal).catch((error) => {
    logger.error({ error }, 'shutdown failed');
    process.exitCode = 1;
  });
  return shutdownPromise;
}

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

Do not call process.exit() at the start of the handler; it can cut off buffered output and pending callbacks. Set process.exitCode and allow the event loop to empty after resources close. A final hard timeout can call process.exit(1) when the platform deadline is near, but log which phase exceeded its budget.

Handle fatal process failures separately

An uncaught exception or unhandled rejection may leave application state uncertain. Record the failure, stop new work and attempt only a short best-effort drain before allowing the supervisor to restart the process. Continuing normal traffic after an unknown invariant failed can be more dangerous than a brief restart.

Supervision belongs outside the process. Use the container platform, systemd or a process manager to restart failed instances with bounded backoff. The application should not fork a replacement for itself while also trying to clean up shared listeners and resources.

Match all timeout layers

The load balancer, orchestrator, process manager, HTTP server, queue visibility timeout and application shutdown deadline must agree. The outer termination grace period should exceed the internal drain budget, which should exceed the normal maximum request or job duration. If a legitimate request can run longer than the deployment permits, redesign it as resumable background work rather than hoping shutdown never occurs.

Example budget

A platform with a 45-second termination grace period might give 5 seconds for endpoint removal, 30 seconds for normal draining and 5 seconds for dependency cleanup, leaving a small emergency margin. These numbers are only a model; production percentiles and queue semantics should determine the actual values.

Do not forget connections and timers

Unref or clear application intervals, stop accepting scheduled tasks, close WebSocket servers and release HTTP client agents. Diagnostic tools that list active handles can help find why a process will not exit, but should not become permanent noisy production output. Database pools and telemetry exporters are frequent causes of an otherwise completed shutdown remaining alive.

Test it under traffic

Send slow and fast requests while restarting the process. Confirm that the load balancer stops routing new traffic, completed requests are not cut off, and the forced timeout remains longer than normal request and job limits.

Deployment verification

  • Continuously request a lightweight endpoint during a rolling deployment and count non-success responses.
  • Run a slow request and verify it completes within the drain window.
  • Keep a WebSocket open and confirm the client receives or performs a clean reconnect.
  • Terminate a worker during a job and prove the job is completed once or becomes safely visible again.
  • Make a dependency close call hang and confirm the hard deadline ends the instance.
  • Inspect logs and metrics for shutdown signal, phase duration, active work and exit result.

Graceful shutdown is part of request correctness. It turns a rolling deployment from an uncontrolled interruption into a bounded state transition that the proxy, application, dependencies and clients can all understand.

Keep the shutdown sequence beside the service architecture and deployment configuration, not only inside one JavaScript file. When request limits, queue leases or platform grace periods change, review the whole timeout chain. A reliable drain is a maintained operational contract rather than a one-time signal handler.

Discuss a project