Node.js Graceful Shutdown & Health Checks: The 2026 Production Guide
Every production Node.js application will eventually be restarted — whether for a new deployment, a scaling event, or a rolling update in Kubernetes. The difference between a smooth restart and a cascade of 502 errors comes down to two often-overlooked patterns: graceful shutdown and health checks.
In 2026, with container orchestration as the default deployment model, getting these patterns right is not optional. A single misconfigured readiness probe or a missing SIGTERM handler can cause dropped requests, data corruption, and frustrated users. This guide covers everything you need to implement production-grade shutdown and health check patterns in Node.js applications — from basic signal handling to advanced Kubernetes probe strategies.
Why Graceful Shutdown Matters
When Kubernetes sends a SIGTERM signal to your Node.js pod, you have a limited window (the terminationGracePeriodSeconds, defaulting to 30 seconds) to finish serving in-flight requests, flush write buffers, close database connections, and exit cleanly. If your application does not handle this signal, the orchestrator sends SIGKILL after the grace period — an immediate, non-catchable termination that can leave database transactions half-committed and WebSocket clients abruptly disconnected.
The Cost of Ungraceful Shutdowns
In a typical microservices architecture handling 10,000 requests per second across 20 pods, a rolling update that restarts 5 pods will interrupt approximately 2,500 in-flight requests if graceful shutdown is not implemented. At a 0.1% error rate SLA, that single deployment burns through your entire monthly error budget in seconds.

Implementing Graceful Shutdown in Node.js
A robust graceful shutdown handler follows five sequential phases: catch the signal, stop accepting new connections, drain in-flight requests, close external resources, and exit with the appropriate status code. Each phase has specific timing requirements and failure modes to account for.
Basic SIGTERM Handler
import { createServer } from 'node:http';
import { once } from 'node:events';
const server = createServer((req, res) => {
// Your request handler
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
});
let isShuttingDown = false;
async function gracefulShutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`Received ${signal} — starting graceful shutdown`);
// Phase 1: Stop accepting new connections
server.close();
console.log('Server closed to new connections');
// Phase 2: Set a hard timeout (safety net)
const forceExit = setTimeout(() => {
console.error('Forced shutdown — timeout exceeded');
process.exit(1);
}, 25_000); // 25s — leave 5s buffer for K8s
forceExit.unref(); // Don't keep event loop alive
try {
// Phase 3: Wait for in-flight requests to complete
await once(server, 'close');
console.log('All connections drained');
// Phase 4: Close external resources
await Promise.allSettled([
closeDatabase(),
closeRedis(),
flushMessageQueue(),
]);
console.log('External resources closed');
// Phase 5: Clean exit
clearTimeout(forceExit);
process.exit(0);
} catch (err) {
console.error('Shutdown error:', err);
process.exit(1);
}
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
server.listen(3000, () => {
console.log('Server listening on port 3000');
});Connection Draining with Keep-Alive
HTTP keep-alive connections present a challenge during shutdown. Clients with persistent connections will continue sending requests on existing sockets even after server.close() is called. To handle this, you need to track active connections and set the Connection: close header on responses during shutdown, signaling clients to open new connections to other pods.
Health Check Endpoints for Kubernetes
Kubernetes uses three types of probes to determine the state of your application: liveness probes, readiness probes, and startup probes. Each serves a distinct purpose, and misconfiguring them is one of the most common causes of production incidents in Node.js deployments.
Liveness Probes — Is the Process Alive?
A liveness probe answers a simple question: is the Node.js process responsive? If a liveness check fails consecutively (based on failureThreshold), Kubernetes restarts the pod. This probe should be extremely lightweight — a simple HTTP 200 response with no dependency checks. Checking databases or external services in a liveness probe is a classic antipattern that causes cascading restarts when a downstream dependency has a brief hiccup.
Readiness Probes — Can It Handle Traffic?
A readiness probe determines whether your pod should receive traffic from the Kubernetes Service. Unlike liveness, readiness checks should verify that critical dependencies are available — your database connection pool is healthy, your Redis cache is reachable, and your application has finished its initialization sequence. When a readiness probe fails, Kubernetes removes the pod from the Service endpoints without restarting it, allowing it to recover naturally.

Production Health Check Implementation
A well-structured health check module exposes three endpoints with different levels of depth. The implementation should be framework-agnostic — whether you are using Express, Fastify, or the native Node.js HTTP module, the health check logic remains the same.
// health-checks.mjs — production health check module
import { Pool } from 'pg';
import { createClient } from 'redis';
const dbPool = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = createClient({ url: process.env.REDIS_URL });
let isReady = false;
let isShuttingDown = false;
export function setReady(ready) { isReady = ready; }
export function setShuttingDown(val) { isShuttingDown = val; }
// Liveness — never check external deps
export async function livenessHandler(req, res) {
if (isShuttingDown) {
res.writeHead(503).end('shutting-down');
return;
}
res.writeHead(200).end('ok');
}
// Readiness — check critical deps
export async function readinessHandler(req, res) {
if (isShuttingDown || !isReady) {
res.writeHead(503).end('not-ready');
return;
}
try {
const checks = await Promise.all([
dbPool.query('SELECT 1').then(() => ({ db: 'ok' })),
redis.ping().then(() => ({ redis: 'ok' })),
]);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ready', checks }));
} catch (err) {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'not-ready', error: err.message }));
}
}Hire Pre-Vetted Node.js Developers
Skip the months-long search. Our exclusive talent network has senior Node.js experts ready to join your team in 48 hours.
Kubernetes Probe Configuration Best Practices
The probe configuration in your Kubernetes deployment manifest has a direct impact on how quickly your service recovers from failures, how rolling updates behave, and whether transient issues cause unnecessary restarts. Getting the timing right requires understanding the interaction between initialDelaySeconds, periodSeconds, timeoutSeconds, and failureThreshold.
Recommended Probe Settings
For a typical Node.js API server that connects to a PostgreSQL database and Redis cache on startup, the following probe configuration provides a good balance between fast failure detection and tolerance for transient issues. The startup probe gives the application up to 60 seconds to initialize, while liveness and readiness checks run at different frequencies reflecting their different purposes.
Coordinating Probes with Graceful Shutdown
When Kubernetes sends SIGTERM, there is a brief race condition: the pod is being terminated, but the endpoints controller has not yet removed it from the Service. During this window (typically 1-2 seconds), new requests can still arrive at the pod. The solution is to immediately fail readiness probes on SIGTERM, which causes Kubernetes to remove the pod from endpoints, while continuing to serve in-flight requests until they complete. This pattern is essential for backend developers building zero-downtime deployment pipelines.
Advanced Shutdown Patterns
Handling WebSocket Connections
WebSocket connections require special handling during shutdown because they are long-lived and bidirectional. Simply closing the HTTP server does not terminate existing WebSocket connections. You need to send a close frame to each connected client with a meaningful status code (1001 for 'going away'), wait a brief period for clients to acknowledge, and then force-close any remaining connections.
Database Transaction Safety
If your application uses connection pooling (as every production Node.js application should), you need to drain the pool gracefully during shutdown. This means waiting for all checked-out connections to be returned, then closing idle connections. For long-running transactions, set a reasonable timeout and roll back any transaction that exceeds it — it is better to roll back cleanly than to leave a transaction in an unknown state.
Message Queue Consumers
Applications consuming from message queues (Kafka, RabbitMQ, SQS) should stop fetching new messages immediately on SIGTERM, finish processing any in-progress messages, commit offsets or acknowledge deliveries, and then disconnect from the broker. Failing to commit offsets before shutdown causes message reprocessing on restart, which is safe for idempotent consumers but can cause duplicate side effects otherwise.
Testing Your Shutdown Logic
Graceful shutdown logic is notoriously difficult to test because it involves process signals, concurrent request handling, and timing-dependent behavior. The most effective approach combines unit tests for individual cleanup functions with integration tests that simulate a full shutdown cycle.
Integration Test with Supertest
A practical integration test starts the server, sends a batch of concurrent requests, sends SIGTERM mid-flight, and verifies that all in-flight requests complete successfully while new requests are rejected with 503. This test catches the most common shutdown bugs: premature exits, connection resets, and resource leaks.
Hire Expert Node.js Developers — Ready in 48 Hours
Building the right system is only half the battle — you need the right engineers to build it. HireNodeJS.com specialises exclusively in Node.js talent: every developer is pre-vetted on real-world projects, API design, event-driven architecture, and production deployments.
Unlike generalist platforms, our curated pool means you speak only to engineers who live and breathe Node.js. Most clients have their first developer working within 48 hours of getting in touch. Engagements start as short-term contracts and can convert to full-time hires with zero placement fee.
Conclusion
Graceful shutdown and health checks are foundational patterns that separate prototype-grade Node.js applications from production-ready systems. Getting them right means zero dropped requests during deployments, faster incident recovery through accurate probe signals, and cleaner resource management that prevents the slow memory leaks and connection exhaustion that plague long-running services.
The patterns covered in this guide — five-phase shutdown, tiered health checks, Kubernetes probe tuning, and WebSocket draining — form the baseline that every production Node.js service should implement. If you are building a team to work on high-availability Node.js systems, HireNodeJS can connect you with engineers who have battle-tested experience with these exact patterns.
Frequently Asked Questions
What happens if a Node.js app does not handle SIGTERM?
If your Node.js application ignores SIGTERM, Kubernetes waits for the terminationGracePeriodSeconds (default 30 seconds) and then sends SIGKILL, which immediately kills the process. Any in-flight requests are dropped, database connections are severed without cleanup, and message queue offsets are not committed.
Should a liveness probe check database connectivity?
No. Liveness probes should only verify that the Node.js process is responsive. Checking databases or external services in a liveness probe causes cascading pod restarts when a downstream dependency has a transient failure, making outages worse rather than better.
How long should the graceful shutdown timeout be?
Set your shutdown timeout to 5 seconds less than the Kubernetes terminationGracePeriodSeconds. For the default 30-second grace period, use a 25-second timeout. This gives Kubernetes enough time to send SIGKILL if your handler stalls.
What is the difference between readiness and liveness probes in Kubernetes?
A liveness probe determines if the container should be restarted (is the process alive?). A readiness probe determines if the container should receive traffic (is it ready to serve requests?). Failed liveness restarts the pod; failed readiness removes it from the service endpoints without restarting.
How do I handle WebSocket connections during graceful shutdown?
Send a WebSocket close frame with status code 1001 (Going Away) to all connected clients, wait 2-3 seconds for acknowledgments, then force-close remaining connections. This allows clients to reconnect to a healthy pod automatically.
How much does it cost to hire a Node.js developer with production deployment experience?
Senior Node.js developers with Kubernetes and production operations experience typically charge between $60-$120/hour depending on region and engagement type. HireNodeJS connects you with pre-vetted developers available within 48 hours at competitive rates.
Vivek Singh is the founder of Witarist and HireNodeJS.com — a platform connecting companies with pre-vetted Node.js developers. With years of experience scaling engineering teams, Vivek shares insights on hiring, tech talent, and building with Node.js.
Need a DevOps-Savvy Node.js Engineer?
HireNodeJS connects you with pre-vetted senior Node.js engineers who understand production patterns — graceful shutdowns, health checks, zero-downtime deployments, and Kubernetes operations. Available within 48 hours, no recruiter fees.
