Graceful Shutdown for Node: Drain Correctly on Kubernetes and ECS

Graceful Shutdown for Node: Drain Correctly on Kubernetes and ECS
14 views
A rolling deploy that SIGKILLs in-flight POSTs is just a distributed partial-failure generator. Kubernetes preStop + terminationGracePeriodSeconds and ECS stop timeouts only help if your Node process actually stops accepting, drains HTTP, nacks queue ownership cleanly, and exits 0 before the grace window. The unfair advantage is a single shutdown orchestrator every entrypoint shares — not ad-hoc process.on('SIGTERM') snippets per service.
⚡ TL;DR: On SIGTERM: fail readiness immediately, stop queue consumers, server.close(), await in-flight with a deadline, flush telemetry, exit. Align K8s preStop sleep with ALB deregistration and ECS stopTimeout. Pair with Node Watch Mode in Staging and Lambda Alias Traffic Shifting for complementary deploy safety.

Graceful Shutdown for Node: production guidance

typescript
// src/shutdown.ts
import type { Server } from "node:http";

type Closer = () => Promise<void>;
const closers: Closer[] = [];
let shuttingDown = false;

export function onShutdown(fn: Closer) { closers.push(fn); }
export function isShuttingDown() { return shuttingDown; }

export function installShutdown(server: Server, budgetMs = 25_000) {
 const run = async (signal: string) => {
 if (shuttingDown) return;
 shuttingDown = true;
 console.log(JSON.stringify({ msg: "shutdown_start", signal }));
 readiness.set(false); // ✅ ALB/K8s stop new traffic
 const timer = setTimeout(() => {
 console.error(JSON.stringify({ msg: "shutdown_timeout" }));
 process.exit(1);
 }, budgetMs);
 try {
 await new Promise<void>((res) => server.close(() => res()));
 for (const c of closers) await c();
 clearTimeout(timer);
 process.exit(0);
 } catch (err) {
 console.error(err);
 process.exit(1);
 }
 };
 process.on("SIGTERM", () => void run("SIGTERM"));
 process.on("SIGINT", () => void run("SIGINT"));
}
typescript
// ❌ Ignoring SIGTERM — K8s waits, then SIGKILL mid-request
// no handler at all

Stop consumers before HTTP drain finishes

typescript
// src/sqs-consumer.ts
import { onShutdown } from "./shutdown.js";

let running = true;
onShutdown(async () => {
 running = false; // ✅ stop long-polling
 await inFlight.drain(); // wait for handlers with AbortSignal
});

export async function loop(poll: () => Promise<Message[]>) {
 while (running) {
 const batch = await poll();
 await Promise.all(batch.map((m) => handle(m, AbortSignal.timeout(10_000))));
 }
}

Align platform timers

Platform Knob Typical
K8s preStop: sleep 5–10 Let endpoint slice propagate
K8s terminationGracePeriodSeconds 30–60 ≥ app budget
ALB deregistration delay Match preStop
ECS stopTimeout ≥ app budget + buffer
Node shutdown budget Leave 5s for SIGKILL margin
yaml
# deployment.yaml excerpt
lifecycle:
 preStop:
 exec:
 command: ["sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 45

Closing checklist

✅ Dos – ✅ Fail readiness first on SIGTERM – ✅ server.close() + await in-flight with deadline – ✅ Stop queue/socket consumers explicitly – ✅ Align preStop, deregistration, and stopTimeout – ✅ Test with chaos: kill pods under load, measure 5xx ❌ Don’ts – ❌ Don’t exit 0 while requests are still writing bodies – ❌ Don’t set grace period shorter than app budget – ❌ Don’t keep readiness green during drain – ❌ Don’t forget open DB pools and telemetry flush

Related reading

Last updated on September 11, 2026 Related: Node.js Cluster Module: Scale Your API to All CPU Cores Without Kubernetes; Node Flamegraphs on ECS: Continuous Profiling That Survives Scale; Node Image Provenance: SLSA Attestations Before ECS or Lambda Pulls.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.