Kontrolowane zamykanie i opróżnianie żądań w toku
Obsługuj SIGTERM, aby podczas wdrożeń opróżniać połączenia, kończyć pracę i poprawnie zamykać zasoby.
Kontrolowane zamykanie i opróżnianie żądań w toku to bezpłatna lekcja Node.js Backend Development Bootcamp na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Node.js Backend Development Bootcamp, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Node.js Backend Development Bootcamp zawiera 4 lekcji w sumie.
Części tej lekcji nie zostały jeszcze przetłumaczone i są wyświetlane po angielsku.
Why Graceful Shutdown Matters
During a deploy, your orchestrator (Kubernetes, ECS, systemd) sends your process a signal and gives it a short window before forcibly killing it. If you exit immediately, you drop in-flight requests, abort half-written database transactions, and return broken responses to users.
A graceful shutdown means:
- Stop accepting new connections.
- Let in-flight requests finish (drain).
- Close resources cleanly: DB pools, message brokers, timers.
- Exit with code
0before the kill timer fires.
This lesson builds a production-grade shutdown path step by step.
The Signals: SIGTERM vs SIGINT vs SIGKILL
Process managers communicate shutdown intent via POSIX signals. You must handle the right ones:
SIGTERM— the polite "please stop" sent by Kubernetes, Docker, and systemd during a rollout. This is the one you handle.SIGINT— sent byCtrl+Cin a terminal. Handle it too for local dev parity.SIGKILL(signal 9) — cannot be caught, blocked, or ignored. The OS terminates you instantly. This is what fires if you miss the grace window.
Node lets you register listeners for catchable signals on process.
process.on('SIGTERM', () => {
console.log('Received SIGTERM, starting graceful shutdown');
shutdown();
});
process.on('SIGINT', () => {
console.log('Received SIGINT, starting graceful shutdown');
shutdown();
});
function shutdown() {
// close servers, drain requests, release resources
}server.close() Drains, It Does Not Kill
The core primitive is server.close([callback]) on an http.Server. Its behavior is exactly what draining requires:
- It stops the server from accepting new connections immediately.
- It keeps existing connections alive until their in-flight requests complete.
- The callback fires only once all connections are closed.
The trap: with HTTP keep-alive, idle connections stay open, so close()'s callback can hang. We solve that shortly. First, the happy path with a complete standalone server.
const http = require('http');
const server = http.createServer((req, res) => {
// simulate a slow in-flight request
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('done\n');
}, 500);
});
server.listen(3000, () => console.log('listening on 3000'));
process.on('SIGTERM', () => {
console.log('SIGTERM: closing server, draining requests');
server.close(() => {
console.log('all connections drained, exiting');
process.exit(0);
});
});The Hard Deadline: Never Drain Forever
Your orchestrator will SIGKILL you when its terminationGracePeriodSeconds elapses (default 30s in Kubernetes). A misbehaving client holding a connection open could otherwise make server.close() hang past that window, turning a clean shutdown into a hard kill.
The rule: always pair draining with a hard timeout that is shorter than the orchestrator's grace period. If draining wins, you exit 0. If the timeout wins, you exit non-zero so the failure is visible.
Use timer.unref() so the timeout itself never keeps the event loop alive.
function gracefulShutdown(server, { timeoutMs = 25000 } = {}) {
let shuttingDown = false;
return () => {
if (shuttingDown) return;
shuttingDown = true;
console.log('draining...');
const forceExit = setTimeout(() => {
console.error('drain timed out, forcing exit');
process.exit(1);
}, timeoutMs);
forceExit.unref();
server.close((err) => {
clearTimeout(forceExit);
if (err) { console.error(err); process.exit(1); }
console.log('drained cleanly');
process.exit(0);
});
};
}Idempotent Shutdown: Guard Against Double Signals
Signals can arrive more than once: an operator hits Ctrl+C twice, or both SIGTERM and SIGINT fire. If shutdown runs twice you'll call server.close() on an already-closing server and may double-close pools, throwing errors mid-shutdown.
Make the handler idempotent with a boolean guard (shown in the previous scene as shuttingDown). The first signal starts the drain; subsequent signals are ignored. Here is a tiny, fully standalone demonstration of the guard pattern you can run directly.
let shuttingDown = false;
function shutdown(reason) {
if (shuttingDown) {
console.log('already shutting down, ignoring ' + reason);
return;
}
shuttingDown = true;
console.log('shutdown started by ' + reason);
}
// simulate two rapid signals
shutdown('SIGTERM');
shutdown('SIGINT');
shutdown('SIGTERM');
console.log('handler ran exactly once');Stop the Load Balancer From Sending New Traffic
There is a race during rollouts: the orchestrator sends SIGTERM at roughly the same time it removes your pod from the load balancer's endpoint list. For a brief moment the LB may still route new requests to a pod that is already draining.
The common fix is a readiness probe that flips to "not ready" the instant shutdown begins. The LB stops sending new traffic before server.close() rejects connections, avoiding spurious 502s.
Flip a flag, let the probe fail, then begin draining (optionally after a short delay so the LB observes the change).
let isShuttingDown = false;
// readiness endpoint the orchestrator polls
app.get('/readyz', (req, res) => {
if (isShuttingDown) {
return res.status(503).send('shutting down');
}
res.status(200).send('ok');
});
process.on('SIGTERM', () => {
isShuttingDown = true; // probe now fails -> LB stops new traffic
setTimeout(beginDrain, 5000); // give the LB a few probe cycles
});Taming HTTP Keep-Alive Connections
HTTP keep-alive keeps TCP connections open between requests. During shutdown an idle keep-alive connection has no in-flight request, yet it prevents server.close()'s callback from firing because the socket is still open.
Two robust options:
- Send
Connection: closeon responses once shutdown begins, so clients stop reusing the socket. - Set
server.keepAliveTimeoutandheadersTimeoutsensibly, and track sockets to destroy idle ones during drain.
Tracking sockets gives you precise control: finish active requests, but forcibly end idle ones.
const http = require('http');
const connections = new Set();
const server = http.createServer((req, res) => {
if (isShuttingDown) res.setHeader('Connection', 'close');
res.end('ok\n');
});
server.on('connection', (socket) => {
connections.add(socket);
socket.on('close', () => connections.delete(socket));
});
function destroyIdleConnections() {
for (const socket of connections) {
if (socket._isIdle) socket.destroy();
}
}Marking Sockets Idle vs Active
To safely destroy only idle sockets, mark a socket active when a request starts and idle when its response finishes. During drain you destroy the ones still idle; active ones are left alone to complete.
This is precisely the strategy battle-tested libraries like stoppable and http-terminator implement for you. Understanding the mechanism lets you debug it when it misbehaves.
const connections = new Set();
server.on('connection', (socket) => {
socket._isIdle = true;
connections.add(socket);
socket.on('close', () => connections.delete(socket));
});
server.on('request', (req, res) => {
req.socket._isIdle = false; // active: a request is in flight
res.on('finish', () => {
req.socket._isIdle = true; // response sent: back to idle
if (isShuttingDown) req.socket.destroy(); // no reuse during drain
});
});
function closeIdle() {
for (const s of connections) if (s._isIdle) s.destroy();
}Closing Downstream Resources in Order
After the HTTP server has drained, release everything else. Order matters: close the HTTP server first so no new work arrives, then drain queues/consumers, then close the database pool last (in-flight handlers may still need it while finishing).
A typical teardown sequence:
server.close()— stop accepting requests, drain HTTP.- Stop message consumers (Kafka/RabbitMQ/BullMQ) so no new jobs start.
- Wait for in-flight jobs, then close broker connections.
await pool.end()— drain and close the DB connection pool.- Flush logs/metrics, then
process.exit(0).
async function shutdown(server, pool, broker) {
await new Promise((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve()))
);
console.log('http drained');
await broker.stopConsuming(); // no new jobs
await broker.drainInFlight(); // finish started jobs
await broker.close();
console.log('broker closed');
await pool.end(); // close DB pool last
console.log('db pool closed');
}A Promisified, Timed Shutdown Orchestrator
Put it together as one reusable function. It races the full async teardown against a hard deadline using Promise.race. Whoever finishes first decides the exit code. This pattern is framework-agnostic and works with Express, Fastify, or a raw http.Server.
Key properties: idempotent via the guard, bounded by timeoutMs, and explicit about success (exit 0) versus forced exit (exit 1).
function installGracefulShutdown(teardown, { timeoutMs = 25000 } = {}) {
let started = false;
const run = async (signal) => {
if (started) return;
started = true;
console.log('shutdown via ' + signal);
const deadline = new Promise((_, reject) => {
const t = setTimeout(() => reject(new Error('timeout')), timeoutMs);
t.unref();
});
try {
await Promise.race([teardown(), deadline]);
console.log('clean shutdown');
process.exit(0);
} catch (err) {
console.error('forced shutdown:', err.message);
process.exit(1);
}
};
process.on('SIGTERM', () => run('SIGTERM'));
process.on('SIGINT', () => run('SIGINT'));
}Don't Forget Crashes: uncaughtException
Graceful shutdown handles deliberate termination. Fatal errors are different. An uncaughtException or unhandledRejection leaves the process in an undefined state; the safe response is to log, attempt a best-effort resource flush, and exit non-zero so the orchestrator restarts you.
Do not try to keep serving traffic after an uncaught exception. The official Node guidance is to treat it as a crash. Here is a complete, runnable illustration of the listener firing exactly once before exit.
let crashing = false;
process.on('uncaughtException', (err) => {
if (crashing) return;
crashing = true;
console.error('uncaught:', err.message);
// best-effort: flush logs/metrics here, then exit non-zero
console.log('exiting with code 1');
process.exit(1);
});
// simulate a fatal bug somewhere deep in the app
setImmediate(() => {
throw new Error('boom: null pointer in handler');
});
console.log('server running, awaiting the crash');Quick Check: The Drain Race
You deploy a Node service to Kubernetes with terminationGracePeriodSeconds: 30. On SIGTERM you immediately call server.close(() => process.exit(0)) and nothing else. In production you still observe occasional 502 errors for clients during rollouts. What is the most likely cause and the correct fix?
Recap: The Graceful Shutdown Checklist
You now have a complete, production-ready shutdown path. The essentials:
- Handle SIGTERM and SIGINT, and make the handler idempotent with a guard.
- Fail the readiness probe first so the load balancer stops sending new traffic before you drain.
- server.close() stops new connections and drains in-flight requests; tame keep-alive by sending
Connection: closeand destroying idle sockets. - Always set a hard timeout shorter than the orchestrator's grace period, with
unref(), exiting non-zero if draining hangs. - Close resources in order: HTTP server, then consumers/broker, then the DB pool, then flush telemetry.
- Treat uncaughtException/unhandledRejection as crashes: best-effort cleanup, then exit non-zero for a restart.
Get these right and zero-downtime deploys stop dropping user requests.
Często zadawane pytania
Czy lekcja „Kontrolowane zamykanie i opróżnianie żądań w toku” jest bezpłatna?
Tak — pełny tekst „Kontrolowane zamykanie i opróżnianie żądań w toku” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Node.js Backend Development Bootcamp, przejdź na CoddyKit PRO. Kurs Node.js Backend Development Bootcamp zawiera 4 lekcji w sumie.
Co nauczysz się w „Kontrolowane zamykanie i opróżnianie żądań w toku”?
Obsługuj SIGTERM, aby podczas wdrożeń opróżniać połączenia, kończyć pracę i poprawnie zamykać zasoby. Ćwiczysz Node.js Backend Development Bootcamp z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Node.js Backend Development Bootcamp?
Nie wymagamy żadnego doświadczenia. Node.js Backend Development Bootcamp w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Kontrolowane zamykanie i opróżnianie żądań w toku”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Node.js Backend Development Bootcamp?
Tak. Każda lekcja Node.js Backend Development Bootcamp zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Limity czasu, ponowienia i wykładniczy backoff z jitterem
- Wzorzec Circuit Breaker i izolacja Bulkhead
- Kontrolowane zamykanie i opróżnianie żądań w toku
- Kontrole zdrowia, sondy gotowości i odrzucanie obciążenia