Graceful shutdown
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.
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+C3. 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.