Graceful shutdown é o encerramento controlado de uma aplicação Node.js. Em vez de interromper o processo imediatamente, o servidor para de aceitar novas requisições, conclui o trabalho em andamento, fecha conexões e encerra dentro de um prazo conhecido.
Esse comportamento é essencial em deploys, autoscaling, reinícios, manutenção e falhas. Sem shutdown correto, usuários recebem conexões resetadas, transações ficam incompletas, mensagens são perdidas e arquivos podem ser corrompidos.
Sinais principais
Em sistemas Unix e containers, os sinais mais comuns são:
SIGTERM: pedido normal de encerramento;SIGINT: interrupção pelo terminal;SIGKILL: encerramento forçado, sem possibilidade de limpeza.
A aplicação deve tratar SIGTERM e SIGINT. SIGKILL não pode ser interceptado.
Servidor HTTP básico
import { createServer } from 'node:http';
const server = createServer(app);
server.listen(3000);
let shuttingDown = false;
async function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
logger.info({ signal }, 'Encerramento iniciado');
server.close(async (error) => {
if (error) {
logger.error({ error }, 'Falha ao fechar servidor');
process.exitCode = 1;
}
await closeResources();
});
}
process.once('SIGTERM', () => shutdown('SIGTERM'));
process.once('SIGINT', () => shutdown('SIGINT'));server.close() para de aceitar novas conexões e espera as conexões atendidas conforme o comportamento do servidor e da versão.
Definindo prazo máximo
const FORCE_EXIT_MS = 30_000;
async function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
const forceTimer = setTimeout(() => {
logger.error({ signal }, 'Shutdown excedeu o prazo');
process.exit(1);
}, FORCE_EXIT_MS);
forceTimer.unref();
try {
await stopAcceptingTraffic();
await drainRequests();
await closeResources();
process.exitCode = 0;
} catch (error) {
logger.error({ error }, 'Shutdown falhou');
process.exitCode = 1;
} finally {
clearTimeout(forceTimer);
}
}O prazo deve ser menor que o tempo concedido pelo orquestrador para evitar SIGKILL antes da limpeza.
Readiness antes do fechamento
Ao iniciar shutdown, marque a aplicação como não pronta. O balanceador deve parar de enviar novas requisições antes de o servidor fechar completamente.
let ready = false;
app.get('/ready', (req, res) => {
if (!ready || shuttingDown) {
res.status(503).json({ status: 'not-ready' });
return;
}
res.json({ status: 'ready' });
});Em Kubernetes, pode existir um pequeno atraso até endpoints serem removidos. Um preStop hook ou período curto de drenagem pode reduzir corridas, mas não substitui o código de shutdown.
Contando requisições ativas
let activeRequests = 0;
app.use((req, res, next) => {
activeRequests += 1;
let finished = false;
function done() {
if (finished) return;
finished = true;
activeRequests -= 1;
}
res.once('finish', done);
res.once('close', done);
next();
});Use essa métrica para observar drenagem. Não dependa somente dela para fechar sockets e tarefas fora do HTTP.
Keep-alive
Conexões keep-alive podem prolongar o encerramento. APIs recentes do servidor HTTP oferecem métodos para fechar conexões ociosas e, em último caso, todas as conexões.
server.close();
server.closeIdleConnections?.();Use fechamento forçado somente após o período de drenagem, pois ele interrompe requisições.
Rastreando sockets
const sockets = new Set();
server.on('connection', (socket) => {
sockets.add(socket);
socket.once('close', () => sockets.delete(socket));
});
function destroyRemainingSockets() {
for (const socket of sockets) socket.destroy();
}Essa técnica ajuda em versões e protocolos específicos, mas pode interferir com upgrades e HTTP/2. Teste o servidor real.
Banco de dados
Depois de drenar requisições, feche pools:
async function closeResources() {
await Promise.allSettled([
database.end(),
redis.quit(),
telemetry.shutdown(),
]);
}Fechar o banco cedo demais faz requisições em andamento falharem. A ordem importa.
Filas e consumidores
Um consumidor deve:
- parar de buscar novas mensagens;
- concluir mensagens em andamento;
- confirmar somente as concluídas;
- devolver ou deixar expirar as restantes;
- fechar conexão.
Defina idempotência porque uma mensagem pode ser reentregue após interrupção.
Tarefas em background
Mantenha um registro de tarefas:
const tasks = new Set();
function track(promise) {
tasks.add(promise);
promise.finally(() => tasks.delete(promise));
return promise;
}
async function drainTasks() {
await Promise.allSettled([...tasks]);
}Não aceite tarefas novas quando shuttingDown for verdadeiro.
Cancelando tarefas longas
const shutdownController = new AbortController();
process.once('SIGTERM', () => {
shutdownController.abort(new Error('Aplicação encerrando'));
shutdown('SIGTERM');
});Propague o signal para Fetch, timers, streams e operações próprias. Algumas tarefas devem concluir; outras podem ser canceladas. Defina por categoria.
WebSockets
WebSockets não terminam com uma resposta HTTP comum. Ao encerrar:
- pare de aceitar upgrades;
- envie mensagem ou close frame;
- aguarde prazo curto;
- termine sockets restantes;
- preserve estado fora do processo.
Clientes devem reconectar com backoff.
Server-Sent Events
Conexões SSE podem durar horas. Notifique o cliente ou feche de forma controlada, permitindo reconexão por Last-Event-ID quando aplicável.
Worker Threads
O pool deve parar de receber tarefas, aguardar as ativas e encerrar workers:
await workerPool.drain({ timeoutMs: 10_000 });
await workerPool.close();Se o prazo expirar, defina se tarefas podem ser retomadas em fila externa.
Processos filhos
Envie SIGTERM aos processos gerenciados, aguarde exit e aplique limite:
child.kill('SIGTERM');
setTimeout(() => child.kill('SIGKILL'), 5000).unref();Considere a árvore de processos e diferenças entre plataformas.
Cluster
O primário coordena workers: deixa de criar novos, envia comando de shutdown e aguarda. Em rolling restart, substitua um worker por vez depois que o novo estiver pronto.
Logs e telemetria
Envie métricas e traces pendentes antes de encerrar. Exportadores possuem métodos de flush e shutdown. Não espere indefinidamente por uma plataforma de observabilidade indisponível.
Ordem recomendada
- marcar not-ready;
- parar entrada de trabalho;
- iniciar prazo máximo;
- drenar HTTP e consumidores;
- concluir ou cancelar tarefas;
- fechar banco e caches;
- flush de telemetria;
- encerrar workers e filhos;
- definir exit code;
- deixar o event loop esvaziar.
Evite process.exit cedo
process.exit() encerra imediatamente e pode descartar logs e I/O pendente. Prefira definir process.exitCode e fechar handles. Use exit forçado apenas no timeout final.
Exceções não tratadas
Em uncaughtException, o estado pode estar inconsistente. Faça logging mínimo e shutdown, sem continuar atendendo tráfego:
process.on('uncaughtException', (error) => {
logger.fatal({ error }, 'Exceção não tratada');
shutdown('uncaughtException');
});Também trate rejeições conforme a política do runtime e corrija a causa.
Idempotência do shutdown
Vários sinais e erros podem acionar o encerramento. Proteja com uma flag ou Promise compartilhada para executar uma única vez.
Kubernetes
Alinhe:
terminationGracePeriodSeconds;- readiness probe;
- preStop, se usado;
- timeout interno;
- duração máxima das requisições;
- timeout do ingress;
- tempo de flush.
O timeout interno deve deixar margem antes do SIGKILL.
Docker
Use forma exec no CMD para que o Node.js receba sinais:
CMD ["node", "dist/server.js"]A forma shell pode criar um processo intermediário. Use init quando a aplicação cria filhos e precisa colher processos.
systemd
Configure o sinal e timeout do serviço de acordo com a aplicação. Não deixe systemd matar antes do prazo interno. O supervisor deve reiniciar processos que encerram com falha.
Testando shutdown
- inicie uma requisição lenta;
- envie SIGTERM;
- confirme que novas requisições são rejeitadas;
- confirme conclusão da requisição ativa;
- verifique fechamento do banco;
- observe exit code;
- repita com prazo excedido;
- teste WebSocket e consumidores.
Métricas
Registre:
- shutdowns por motivo;
- duração total;
- requisições drenadas;
- tarefas canceladas;
- timeouts forçados;
- erros durante limpeza;
- sockets restantes;
- exit code.
Erros comuns
- chamar process.exit imediatamente;
- fechar banco antes das requisições;
- não marcar not-ready;
- não definir prazo;
- ignorar keep-alive e WebSockets;
- não parar consumidores;
- não propagar cancelamento;
- executar shutdown várias vezes;
- usar CMD em forma shell;
- não testar em ambiente real.
Fluxo recomendado
Implemente shutdown idempotente, marque not-ready, pare entradas, drene trabalho, feche recursos na ordem e aplique prazo. Combine com AbortController, Cluster, child_process e Process Reports.
Consulte a documentação oficial de server.close e a referência oficial de sinais do processo.


