Graceful Shutdown
Manipule SIGTERM e SIGINT parando novos trabalhos, esvaziando conexões HTTP e fechando pools de banco de dados antes de sair no Node.js 24.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
import express from "express";
import { Pool } from "pg";
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const server = app.listen(Number(process.env.PORT ?? 3000), "0.0.0.0");
let shuttingDown = false;
app.get("/ready", (_req, res) => {
if (shuttingDown) return res.status(503).json({ status: "draining" });
res.json({ status: "ready" });
});
function shutdown(signal: string) {
shuttingDown = true;
console.log(JSON.stringify({ event: "shutdown_start", signal }));
server.close(async () => {
await pool.end();
console.log(JSON.stringify({ event: "shutdown_complete" }));
process.exit(0);
});
setTimeout(() => {
console.error(JSON.stringify({ event: "shutdown_forced" }));
process.exit(1);
}, 30_000).unref();
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));Quando usar isso: Todo serviço HTTP de produção no Kubernetes, ECS, Cloud Run, PM2 ou systemd.
Exemplo de Trabalho
// src/shutdown.ts
import type { Server } from "node:http";
import type { Pool } from "pg";
export type Closable = { close: () => Promise<void> };
export function registerGracefulShutdown(
server: Server,
resources: Closable[],
options: { timeoutMs?: number } = {}
) {
const timeoutMs = options.timeoutMs ?? 30_000;
let draining = false;
const shutdown = async (signal: string) => {
if (draining) return;
draining = true;
console.log(JSON.stringify({ event: "shutdown_start", signal }));
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
for (const r of resources) {
await r.close();
}
console.log(JSON.stringify({ event: "shutdown_complete" }));
process.exit(0);
};
const forceTimer = setTimeout(() => {
console.error(JSON.stringify({ event: "shutdown_timeout" }));
process.exit(1);
}, timeoutMs);
forceTimer.unref();
process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
return {
isDraining: () => draining,
};
}
// src/main.ts
import express from "express";
import { Pool } from "pg";
import { registerGracefulShutdown } from "./shutdown.js";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const app = express();
const shutdownCtl = registerGracefulShutdown(
app.listen(3000, "0.0.0.0"),
[{ close: () => pool.end() }]
);
app.get("/ready", (_req, res) => {
if (shutdownCtl.isDraining()) {
res.status(503).json({ status: "draining" });
return;
}
res.json({ status: "ready" });
});O que isso demonstra:
server.close()para de aceitar; finaliza requisições em andamentopool.end()esvazia conexões de banco de dados de forma limpa- A prontidão falha durante o esvaziamento para que os balanceadores de carga parem de enviar tráfego
- Saída forçada após 30s corresponde ao
terminationGracePeriodSecondstípico
Análise Detalhada
Linha do Tempo de Sinais no Kubernetes
1. Pod marcado como Terminando
2. Endpoints removidos do Service (prontidão falha)
3. hook preStop é executado (sleep opcional)
4. SIGTERM enviado ao container
5. Período de graça expira -> SIGKILL
Alinhe o tempo limite de desligamento do aplicativo com terminationGracePeriodSeconds do pod (tempo limite do aplicativo < período de graça).
Desligamento do Fastify
import Fastify from "fastify";
const app = Fastify();
app.addHook("onClose", async () => {
await pool.end();
});
const close = async () => {
await app.close();
process.exit(0);
};
process.on("SIGTERM", () => void close());app.close() aciona hooks onClose na ordem inversa de registro.
Trabalho em Andamento Além do HTTP
| Recurso | API de Fechamento |
|---|---|
PostgreSQL pg.Pool | pool.end() |
Redis ioredis | redis.quit() |
| Worker BullMQ | worker.close() |
Cron setInterval | clearInterval + aguardar último job |
Monitore jobs em segundo plano; server.close() sozinho não espera por workers de fila.
@godaddy/terminus
import { createTerminus } from "@godaddy/terminus";
createTerminus(server, {
signals: ["SIGTERM", "SIGINT"],
healthChecks: { "/health": () => Promise.resolve() },
onSignal: async () => { await pool.end(); },
});Agrupa verificações de saúde e desligamento para aplicativos Express.
Armadilhas
- Sem falha de prontidão no desligamento - LB envia tráfego para pod moribundo. Correção: flag
shuttingDownem/ready. server.close()sem timeout - trava para sempre em keep-alive. Correção: timer de saída forçada; ajustekeepAliveTimeoutno servidor.- Fechamento do pool antes do esvaziamento HTTP - requisições em andamento geram erro. Correção:
server.close()primeiro, depois os pools. - SIGTERM ignorado em dev - Ctrl+C envia SIGINT; manipule ambos.
- Workers continuam consumindo após o HTTP parar - processamento duplicado durante a implantação. Correção: pausar workers no SIGTERM.
kill_timeoutdo PM2 muito curto - kill forçado no meio da requisição. Correção:kill_timeoutde 30s - PM2 & systemd.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Manipulador SIGTERM manual | Controle total, qualquer framework | Quer saúde e esvaziamento "batteries-included" |
| @godaddy/terminus | Serviços HTTP Express | Fastify (use app.close()) |
| Apenas Kubernetes (sem manipulador de app) | Nunca | APIs de produção (sempre manipule SIGTERM) |
process.exit(0) imediato | Jobs em lote | Serviços HTTP (causa 502s) |
FAQs
Quanto tempo deve durar o sleep do preStop?
3-5 segundos geralmente são suficientes para propagação de endpoints antes do SIGTERM. Ajuste com a documentação do balanceador de carga.
O Cloud Run envia SIGTERM?
Sim, ao reduzir a escala da instância e implantar uma revisão. O mesmo padrão de manipulador se aplica.
Lambda precisa de desligamento gracioso?
Invocações de curta duração; callbackWaitsForEmptyEventLoop é mais importante que SIGTERM. Veja Padrões de Manipulador Lambda.
Conexões WebSocket?
Feche o servidor WebSocket na sequência de desligamento; notifique os clientes com um frame de fechamento. Pode precisar de um período de graça mais longo.
NestJS?
app.enableShutdownHooks() escuta o SIGTERM e fecha módulos que implementam OnModuleDestroy.
Qual código de saída em desligamento forçado?
Saia com 1 em caso de timeout para que o supervisor registre a terminação anormal e os alertas sejam disparados.
Relacionados
- Implantações Zero-Downtime - rollout + drain
- Probes de Saúde e Prontidão - prontidão durante o drain
- Implantação no Kubernetes - preStop e período de graça
- Noções Básicas de Operações de Runtime - modelo de supervisão
- Melhores Práticas de Operações de Runtime - checklist da seção
Versões da Stack: Esta página foi escrita para Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 e NestJS 11.