Graceful Shutdown
On SIGTERM, stop accepting new connections, let in-flight requests finish, close the DB, then exit — with a hard timeout as a backstop.
When your platform deploys or scales down, it sends your process SIGTERM and starts a countdown. If you ignore it, the platform eventually SIGKILLs you mid-request — dropped responses, half-written transactions, angry users. A graceful shutdown handles this window deliberately.
The choreography
- Catch
SIGTERM(andSIGINTfor local Ctrl-C). - Call
server.close()— it stops accepting new connections but lets in-flight requests drain. - Close your database pool and other resources after the server has drained.
- Arm a hard timeout: if draining hangs, force-exit so you do not get
SIGKILLed uncleanly.
process.on('SIGTERM', () => {
server.close(() => { db.end(); process.exit(0); });
setTimeout(() => process.exit(1), 10_000).unref();
});Example
import express from 'express';
const app = express();
let shuttingDown = false;
// Readiness probe flips first so the LB drains traffic before we close.
app.get('/health/ready', (req, res) => {
res.status(shuttingDown ? 503 : 200).json({ ready: !shuttingDown });
});
app.get('/health/live', (req, res) => res.json({ live: true }));
const server = app.listen(process.env.PORT ?? 3000);
async function shutdown(signal) {
console.log(`${signal} received — draining`);
shuttingDown = true; // /health/ready now returns 503
// Backstop: if draining hangs, exit anyway before the platform SIGKILLs us.
const hardStop = setTimeout(() => {
console.error('Drain timed out, forcing exit');
process.exit(1);
}, 10_000);
hardStop.unref();
server.close(async () => {
try {
await db.end(); // close pools only AFTER requests have drained
clearTimeout(hardStop);
process.exit(0);
} catch {
process.exit(1);
}
});
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));When to use it
- A Kubernetes deployment sends SIGTERM before terminating a pod; the Express app intercepts it to stop accepting connections and drain in-flight requests.
- A developer stores the server handle returned by app.listen() to call server.close() on shutdown signals, preventing new connections while existing ones finish.
- A database connection pool is explicitly closed in the SIGTERM handler so no queries are left open when the process exits.
More examples
Basic SIGTERM graceful shutdown
Intercepts SIGTERM, calls server.close() to stop accepting new connections, and exits once in-flight requests complete.
const app = require('./app');
const server = app.listen(process.env.PORT || 3000, () => console.log('Started'));
process.on('SIGTERM', () => {
console.log('SIGTERM received — shutting down');
server.close(() => {
console.log('All connections closed');
process.exit(0);
});
});Drain connections with timeout
Sets a forced exit timeout of 10 seconds as a safety net, closes the HTTP server, then drains the database pool.
async function shutdown(server) {
console.log('Shutting down...');
await new Promise((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()));
});
await db.pool.end(); // close DB connections
console.log('Clean exit');
process.exit(0);
}
const FORCE_EXIT_MS = 10_000;
process.on('SIGTERM', () => {
setTimeout(() => process.exit(1), FORCE_EXIT_MS).unref();
shutdown(server);
});Handle SIGINT and SIGTERM
Registers the same graceful shutdown logic for both SIGTERM (from Kubernetes) and SIGINT (from Ctrl+C in development).
function graceful(signal) {
return () => {
console.log(`${signal} received`);
server.close(() => {
console.log('Server closed');
process.exit(0);
});
};
}
process.on('SIGTERM', graceful('SIGTERM'));
process.on('SIGINT', graceful('SIGINT')); // Ctrl+C in dev
Discussion