Graceful shutdown in nodejs
advance · Node.js — Server-side JavaScript
A Graceful Shutdown is the process of stopping a server while ensuring that all currently active tasks are completed and resources are cleaned up before the process exits. Without this, you risk dropping active user requests, corrupting database transactions, or leaving "zombie" processes behind. In Node.js, this usually involves listening for POSIX signals (like SIGTERM or SIGINT ) and executing a specific sequence of cleanup steps. 1. The Shutdown Sequence To shut down properly, your application should follow these steps in order: Receive Signal: The OS or Orchestrator (like Kubernetes/PM2) sends a signal to the process. Stop Inbound Traffic: Stop the HTTP server from accepting new connections. Wait for In-flight Requests: Allow existing requests (like a pending payment or file upload) a few seconds to finish. Close Resources: Shut down database pools, close Redis connections, and flush logs to disk. Exit Process: Finally, exit with a success code ( 0 ) or an error code ( 1 ). 2. Implementation Example (Express & MongoDB) Here is how you implement a robust graceful shutdown handler: const express = require('express'); const mongoose = require('mongoose'); const app = express(); const server = app.listen(3000); // Helper function to close all resources async function handleShutdown(signal) { console.log(`\nReceived ${signal}. Starting graceful shutdown...`); // 1. Stop the server from accepting new requests server.close(() => { console.log('HTTP server closed.'); }); // 2. Set a forced timeout (The "Safety Net") // If cleanup takes too long, force exit to prevent "zombie" processes const forceExitTimeout = setTimeout(() => { console.error('Forced shutdown: Cleanup took too long.'); process.exit(1); }, 10000); // 10 seconds try { // 3. Close Database Connections await mongoose.connection.close(); console.log('MongoDB connection closed.'); // 4. Clear the safety timeout and exit clearTimeout(forceExitTimeout); console.log('Shutdown complete. Goodbye!'); process.exit(0); } catch (err) { console.error('Error during shutdown:', err); process.exit(1); } } // Listen for termination signals process.on('SIGTERM', () => handleShutdown('SIGTERM')); // Sent by Kubernetes/PM2 process.on('SIGINT', () => handleShutdown('SIGINT')); // Sent by Ctrl+C 3. Key Signals to Handle Signal Origin Typical Meaning SIGTERM Kubernetes / PM2 / Docker "Please stop gracefully as soon as possible." SIGINT User (Ctrl+C) "Interrupting the process from the terminal." SIGHUP Terminal Closure "The controlling terminal has closed." SIGKILL OS ( kill -9 ) Cannot be caught. The OS kills the process immediately. 4. Common Pitfalls The "Keep-Alive" Problem Modern browsers use Keep-Alive to keep a connection open for multiple requests. Even if you call server.close() , these idle connections might stay open, preventing the callback from ever firing. The Fix: Modern Node.js versions (v18.2+) include server.closeIdleConnections() which helps terminate these idle sockets immediately while letting active ones finish. PID 1 in Docker If you run your app in Docker using CMD node index.js , Node.js becomes PID 1 . By default, Linux does not forward signals (like SIGTERM ) to PID 1 unless the process is explicitly coded to handle them. The Fix: Always use a lightweight init system like tini or dumb-init in your Dockerfile to ensure signals are passed correctly to your app. Long-running Tasks If you have a task that takes 2 minutes (e.g., a massive image resize), your orchestrator (like Kubernetes) might only wait 30 seconds before sending a SIGKILL . The Fix: Use a Work Queue (like BullMQ) instead of doing heavy tasks directly in an HTTP request handler. This allows the worker to finish the current job independently of the web server shutting down.