O Modelo de Resiliência do Node.js
Resiliência é a prática deliberada de limitar o quão longe uma falha pode se espalhar por um sistema, em vez de assumir - ou esperar - que cada dependência downstream sempre responda corretamente e no prazo.
Busque em todas as páginas da documentação
Resiliência é a prática deliberada de limitar o quão longe uma falha pode se espalhar por um sistema, em vez de assumir - ou esperar - que cada dependência downstream sempre responda corretamente e no prazo.
A seção de resiliência cobre um conjunto específico de padrões: timeouts, retries com backoff, circuit breakers e graceful degradation.
Lidos individualmente, eles podem parecer uma caixa de ferramentas arbitrária; lidos como um único modelo, eles são, na verdade, uma sequência de perguntas que uma requisição faz sobre uma dependência com problemas, cada padrão respondendo à pergunta que o anterior não conseguiu.
Noções Básicas de Resiliência mostra código em funcionamento para cada padrão; esta página é o raciocínio que os conecta e explica por que a ordem em que são aplicados importa tanto quanto quais deles você escolhe.
Um domínio de falha é o limite dentro do qual uma falha é contida - o conjunto de coisas que quebram juntas quando uma coisa quebra.
Sem um design deliberado, os domínios de falha de um backend Node são muito maiores do que precisam ser: uma única consulta lenta ao banco de dados pode esgotar seu pool de conexões, o que paralisa todas as outras requisições que usam esse pool, o que se propaga até que seu serviço falhe completamente - um dia ruim de uma dependência se torna o dia ruim de todo o seu processo.
Padrões de resiliência existem para encolher os domínios de falha de volta ao tamanho do problema real.
Uma analogia útil são os compartimentos estanques de um navio: uma brecha no casco em um compartimento inunda aquele compartimento e o próximo, mas as portas entre as seções são construídas especificamente para que o navio como um todo permaneça à tona em vez de um único furo afundá-lo completamente.
Cada padrão de resiliência nesta seção é um tipo diferente de compartimento, respondendo a uma pergunta diferente em uma sequência que é, na verdade, fixa:
Pular uma camada anterior não apenas remove essa proteção - ela mina as camadas após ela: retries sem um timeout podem tentar novamente uma chamada que ainda está pendente da primeira tentativa, e um circuit breaker sem um bulkhead ainda pode ser privado de capacidade por chamadas concorrentes se acumulando antes que ele dispare.
A sequência acima não é apenas uma lista - é um fluxo de controle, e vê-la como um único diagrama esclarece por que cada camada existe:
request
│
▼
┌─────────────┐ exceeds limit ┌──────────────┐
│ timeout │ ─────────────────▶ │ fail/retry │
└─────────────┘ └──────┬───────┘
│ retries exhausted, or
│ error rate crosses threshold
▼
┌──────────────┐
│ circuit │
│ breaker │
└──────┬───────┘
│ circuit open, or
│ dependency non-critical
▼
┌──────────────┐
│ graceful │
│ degradation │
└──────────────┘
O state machine interno de um circuit breaker é o mecanismo que vale a pena detalhar precisamente, porque seu estado intermediário é a parte que as pessoas geralmente erram.
type BreakerState = "closed" | "open" | "half-open";
// closed: calls pass through normally; failures are counted
// open: calls fail immediately, no request reaches the dependency
// half-open: exactly one trial call is allowed through, to test recovery
// - success -> closed again; failure -> open again, cooldown resetsClosed é o estado normal - as chamadas passam e as falhas são apenas contadas em direção a um limite.
Open é o estado de falha contida - o breaker falha imediatamente em todas as chamadas, sem sequer tentar a requisição de rede, que é o que realmente protege a própria capacidade do seu serviço (threads, sockets, tempo do event-loop) de ser consumida por chamadas a algo que já se sabe que está inativo.
Half-open existe porque um breaker que simplesmente volta para closed após um cooldown fixo corre o risco de sobrecarregar uma dependência que está se recuperando com todo o seu tráfego de uma vez; permitir que uma (ou um pequeno número controlado de) chamada(s) de teste passe(m) primeiro responde "ela realmente se recuperou?" antes de comprometer o resto do seu tráfego de volta a ela.
É aqui que retries e circuit breakers interagem de uma forma que é fácil de inverter: um loop de retry que continua tentando contra um circuito aberto é um trabalho inútil, já que o breaker já está falhando rapidamente por design - a lógica de retry deve verificar o estado do breaker, não tentar novamente cegamente sob ele.
Backoff - esperar progressivamente mais entre as tentativas de retry, geralmente com um jitter aleatório adicionado - existe para evitar que cada cliente com falha tente novamente no exato momento em que uma dependência com problemas está tentando se recuperar, o que, de outra forma, transformaria um breve lapso em uma onda sincronizada de carga renovada (uma retry storm) exatamente quando a dependência menos pode lidar com isso.
Em escala, o maior modo de falha contra o qual os padrões de resiliência protegem não é uma única dependência caindo - é a falha em cascata, onde uma desaceleração em um serviço se propaga através de cadeias de chamadas síncronas até que uma parte não relacionada do sistema falhe também, puramente porque estava esperando por algo que estava esperando por outra coisa.
Bulkheads (limitar chamadas concorrentes a uma dependência específica, via semáforo, pool de conexões dedicado ou limite de workers) existem especificamente para parar essa propagação no ponto de contato: um fornecedor lento pode esgotar sua própria capacidade alocada sem tocar na capacidade reservada para todo o resto que seu serviço faz.
Chaos engineering - injetar deliberadamente falhas (instâncias mortas, latência adicionada, conexões perdidas) em um sistema para observar se seus padrões de resiliência realmente se comportam como projetado - existe porque esses padrões são notoriamente difíceis de verificar apenas lendo código; o limite de um circuit breaker, a duração de um timeout e o backoff de um retry interagem de maneiras que são muito mais fáceis de errar no papel do que observar sob uma falha injetada.
Observabilidade não é opcional aqui: cada camada de resiliência precisa de seu próprio sinal para ser útil operacionalmente - uma taxa crescente de timeouts, transições de estado open/half-open de um circuit breaker, uma contagem crescente de retries - porque sem eles, um padrão de resiliência fazendo seu trabalho silenciosamente (contendo uma falha) pode parecer idêntico do lado de fora a um padrão de resiliência que silenciosamente não está ajudando em nada.
Onde essa lógica realmente vive também mudou ao longo da história da indústria: serviços Node iniciais criavam loops de retry inline; bibliotecas como opossum e cockatiel padronizaram a lógica de circuit breaker e retry como componentes reutilizáveis e testáveis; e cada vez mais, service meshes (Envoy, Istio) implementam timeouts, retries e circuit breaking na camada de infraestrutura, completamente fora do código da aplicação, o que troca a flexibilidade em nível de aplicação por consistência imposta em todos os serviços, independentemente da linguagem.
| Onde a Lógica de Resiliência Vive | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Código da aplicação inline | Controle total; sem nova dependência de infraestrutura | Fácil de errar sutilmente; inconsistente entre serviços/equipes | Pequenos serviços, ou lógica que necessita de nuances específicas do negócio |
| Biblioteca (opossum, cockatiel) | Semântica de falha testada, reutilizável e documentada | Ainda configurado e de propriedade por serviço; pode divergir entre serviços | A maioria dos serviços Node - a escolha padrão |
| Service mesh / sidecar | Política consistente em todos os serviços, agnóstica à linguagem | Infraestrutura real para executar e operar; controle menos granular por chamada | Grandes arquiteturas multisserviço e poliglota |
A prática deliberada de limitar o quão longe uma falha se espalha pelo seu sistema - através de timeouts, retries, circuit breakers, bulkheads e graceful degradation - em vez de assumir que toda dependência sempre responderá corretamente e no prazo.
Porque cada um responde a uma pergunta que o anterior deixa em aberto: um timeout limita quanto tempo esperamos, um retry decide se tenta novamente, um circuit breaker decide quando parar de tentar completamente, e graceful degradation decide o que o usuário vê quando nada funcionou. Pular uma camada anterior mina as que vêm depois - tentar novamente sem um timeout, por exemplo, pode tentar novamente uma chamada que ainda está pendente.
Após um período de cooldown no estado open, o breaker permite que um pequeno número controlado de chamadas de teste passe em vez de retomar o tráfego total imediatamente. Se essas chamadas de teste forem bem-sucedidas, o breaker fecha e o tráfego normal é retomado; se falharem, ele reabre e o cooldown reinicia - isso evita sobrecarregar uma dependência que está se recuperando com carga total de uma vez.
Porque você não pode saber, a partir de um timeout ou de uma conexão perdida, se a requisição original realmente foi bem-sucedida no servidor antes que a resposta fosse perdida - tentar novamente pode executar a mesma operação (como uma cobrança de pagamento) uma segunda vez. Retries só são seguros quando a operação é idempotente, ou é protegida por uma chave de idempotência explícita na qual o servidor pode deduplicar.
Um circuit breaker decide se deve tentar uma chamada, com base no histórico recente de falhas dessa dependência. Um bulkhead limita quantas chamadas podem estar em andamento ao mesmo tempo para uma determinada dependência, independentemente de estarem tendo sucesso - portanto, um bulkhead protege sua própria capacidade mesmo de uma dependência que está apenas lenta em vez de falhando completamente.
Sem jitter, cada cliente que falhou no mesmo momento tenta novamente no mesmo momento novamente, criando uma onda sincronizada de carga (uma retry storm) exatamente quando uma dependência em recuperação menos pode lidar com isso. O jitter aleatório espalha essas retries ao longo do tempo, suavizando a carga em vez de concentrá-la.
Sim - resiliência sempre troca alguma latência adicionada e complexidade de código por contenção, então uma chamada para uma dependência interna genuinamente confiável e de baixo risco, com acoplamento apertado ao seu chamador, pode não precisar da sequência completa. A decisão é sobre quais modos de falha são plausíveis e custosos o suficiente para valer a pena se proteger, não sobre aplicar todos os padrões uniformemente em todos os lugares.
Uma falha de dependência única é contida se os padrões de resiliência estiverem funcionando; uma falha em cascata é o que acontece quando essa contenção não se sustenta, e uma desaceleração em um serviço se propaga através de cadeias de chamadas síncronas até que partes não relacionadas do sistema falhem também, puramente por esperar por algo que está esperando por outra coisa.
Porque uma camada de resiliência funcionando corretamente (contendo silenciosamente uma falha) e uma que silenciosamente falha em ajudar podem parecer idênticas do lado de fora sem sinais dedicados - uma taxa de timeout, transições de estado de circuit breaker, uma contagem de retry - para distingui-las. Sem esses sinais, você não pode verificar se os padrões estão realmente fazendo seu trabalho.
Um service mesh (como Envoy ou Istio) impõe políticas consistentes de timeout, retry e circuit breaking em todos os serviços, independentemente da linguagem, ao custo de executar e operar infraestrutura real e perder parte do controle granular e específico do negócio que a lógica de aplicação inline permite.
Não - é uma técnica de verificação, não de design. Injetar deliberadamente falhas (instâncias mortas, latência adicionada) testa se suas durações de timeout, políticas de retry e limites de breaker realmente se comportam como pretendido em conjunto, mas você ainda precisa raciocinar sobre a sequência e as interações para projetá-los em primeiro lugar.
Feito bem: uma página de produto retorna a uma lista de recomendações em cache, com uma nota visível, quando o serviço de recomendação ao vivo está inativo. Feito mal: a mesma página omite silenciosamente as recomendações sem qualquer indicação de que algo está errado, deixando usuários e engenheiros de plantão sem saber que uma dependência falhou.
Versões da Stack: Esta página foi escrita para Node.js 24 LTS e TypeScript 5.6+.
Revisado por Chris St. John·Última atualização: 15 de jul. de 2026