APIs Node.js raramente funcionam sozinhas. Elas dependem de bancos de dados, serviços de pagamento, filas, sistemas de autenticação e APIs de terceiros. Quando uma dessas dependências fica lenta ou indisponível, continuar enviando requisições sem controle pode consumir conexões, aumentar o tempo de resposta e provocar uma falha em cascata. O padrão Circuit Breaker no Node.js reduz esse risco ao interromper temporariamente chamadas para um serviço que já demonstra sinais claros de falha.
O nome vem de um disjuntor elétrico: quando há sobrecarga, o circuito é aberto para proteger o restante da instalação. Em software, o circuit breaker observa as tentativas, contabiliza falhas e, ao atingir um limite, passa a rejeitar novas chamadas imediatamente. Depois de um período de espera, ele permite algumas requisições de teste para descobrir se a dependência se recuperou.
Neste guia, você aprenderá os estados do padrão, como implementar uma versão simples com JavaScript, integrar timeouts e AbortController, evitar sobrecarga durante a recuperação, criar fallbacks, registrar métricas e testar o comportamento com segurança.
Por que falhas externas se espalham?
Imagine uma rota que consulta um serviço de catálogo. Em condições normais, a chamada termina em 100 milissegundos. Durante uma falha, cada requisição passa a aguardar 20 segundos antes de retornar erro. Se centenas de clientes acessarem a rota ao mesmo tempo, a aplicação acumula Promises, sockets e memória enquanto espera por uma dependência que provavelmente continuará falhando.
Mesmo que o processo Node.js não bloqueie a thread principal durante a espera de rede, os recursos envolvidos continuam ocupados. O pool de conexões pode esgotar, os timeouts do proxy podem vencer e outras rotas saudáveis passam a sofrer. Esse efeito é chamado de falha em cascata.
Antes de aplicar o padrão, revise o que é Node.js, o guia para criar uma API com Node.js e as práticas de performance em APIs Node.js.
Os três estados do circuit breaker
Um circuit breaker normalmente possui três estados:
- Closed: o circuito está fechado e as chamadas passam normalmente. Falhas são contabilizadas.
- Open: o limite de falhas foi atingido. Novas chamadas são rejeitadas imediatamente, sem acessar a dependência.
- Half-open: depois do período de espera, uma quantidade limitada de chamadas é liberada para testar a recuperação.
Se uma chamada de teste funciona, o circuito volta para closed e os contadores são reiniciados. Se falha, ele retorna para open e inicia uma nova janela de espera.
Quando usar Circuit Breaker no Node.js
O padrão é útil quando a aplicação chama um recurso remoto ou potencialmente instável, como APIs de pagamento e logística, serviços internos, bancos de dados acessados por rede, provedores de e-mail, sistemas de armazenamento, serviços de inteligência artificial, filas e caches externos.
Ele não substitui validação, tratamento de erros ou monitoramento. Também não deve esconder falhas de programação locais. Se uma função lança erro por causa de uma variável indefinida, abrir o circuito apenas mascara um defeito que precisa ser corrigido.
Implementação básica em JavaScript
A classe abaixo apresenta uma implementação didática:
class CircuitBreaker {
constructor(action, options = {}) {
this.action = action;
this.failureThreshold = options.failureThreshold ?? 5;
this.resetTimeout = options.resetTimeout ?? 30000;
this.state = 'CLOSED';
this.failures = 0;
this.nextAttempt = 0;
}
async execute(...args) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuito aberto');
}
this.state = 'HALF_OPEN';
}
try {
const result = await this.action(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures += 1;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
}A classe recebe uma função assíncrona e opções de limite. Enquanto o circuito está fechado, cada falha incrementa o contador. Ao atingir o limite, o estado muda para aberto. Durante esse período, a função original não é executada.
Protegendo uma chamada com fetch()
async function fetchCatalog() {
const response = await fetch('https://api.example.com/catalog', {
signal: AbortSignal.timeout(3000)
});
if (!response.ok) {
throw new Error(`Catálogo respondeu ${response.status}`);
}
return response.json();
}
const catalogBreaker = new CircuitBreaker(fetchCatalog, {
failureThreshold: 4,
resetTimeout: 20000
});
async function loadCatalog() {
return catalogBreaker.execute();
}O timeout é essencial. Sem ele, uma chamada que nunca termina também impede o circuit breaker de registrar rapidamente a falha. O artigo sobre AbortController no Node.js mostra como cancelar operações assíncronas e combinar diferentes sinais.
Quais erros devem contar?
Nem todo erro deve abrir o circuito. Uma resposta 400, por exemplo, pode indicar que a própria aplicação enviou dados inválidos. Repetir a chamada não ajudará, mas esse problema também não significa necessariamente que o serviço remoto esteja indisponível.
Normalmente entram no cálculo timeouts, falhas de conexão, respostas HTTP 500, 502, 503 e 504, limites de conexão esgotados e erros temporários claramente identificados. Erros de autenticação, validação e autorização devem seguir tratamento próprio. Uma implementação mais completa recebe uma função shouldCountFailure(error) para decidir se o evento afeta o circuito.
Usando uma janela de falhas
Contar falhas consecutivas é simples, mas pode ser rígido. Cinco erros distribuídos ao longo de várias horas não representam a mesma situação que cinco erros em dez segundos. Sistemas reais costumam analisar uma janela móvel, combinando quantidade mínima de requisições e taxa de erro.
Um critério possível é abrir o circuito quando houver pelo menos 20 chamadas na janela e mais de 50% delas falharem. Isso evita abrir o circuito por duas falhas isoladas em um serviço pouco utilizado.
Controle no estado half-open
Quando termina o período de espera, não libere imediatamente todas as requisições acumuladas. A dependência pode estar se recuperando e receber uma nova onda de tráfego capaz de derrubá-la novamente.
Permita uma ou poucas chamadas de teste. Enquanto o teste estiver em andamento, outras requisições devem receber fallback ou erro imediato. Esse limite evita o chamado thundering herd, em que muitos clientes retomam ao mesmo tempo.
if (this.state === 'HALF_OPEN' && this.probeInProgress) {
throw new Error('Teste de recuperação em andamento');
}
this.probeInProgress = true;
try {
const result = await this.action(...args);
this.onSuccess();
return result;
} finally {
this.probeInProgress = false;
}Fallbacks úteis
Quando o circuito está aberto, a aplicação pode usar dados recentes armazenados em cache, retornar uma resposta parcial, colocar a operação em uma fila, usar um provedor secundário ou informar indisponibilidade temporária com status 503.
O fallback precisa ser explícito. Não retorne informação antiga como se fosse atual. Inclua campos como stale: true ou uma mensagem clara. Para estratégias de armazenamento temporário, consulte Redis com Node.js para cache e filas.
Circuit breaker não substitui retry
Retry e circuit breaker resolvem problemas diferentes. Uma nova tentativa pode superar uma falha transitória. O circuit breaker impede insistência quando as falhas já indicam um problema persistente.
Quando usar os dois, faça poucas tentativas com atraso exponencial e aleatoriedade. Depois, registre apenas o resultado final no circuit breaker. Se cada tentativa interna for contada como uma falha independente, uma única requisição pode abrir o circuito rapidamente.
async function retry(action, attempts, signal) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt++) {
signal?.throwIfAborted();
try {
return await action();
} catch (error) {
lastError = error;
if (attempt === attempts) break;
const wait = 200 * 2 ** (attempt - 1);
await new Promise(resolve => setTimeout(resolve, wait));
}
}
throw lastError;
}Bibliotecas prontas
Em produção, uma biblioteca madura pode oferecer janelas estatísticas, fallbacks, eventos e limites de concorrência. O projeto Opossum é uma opção conhecida para Node.js. A documentação do padrão em Microsoft Azure Architecture Center apresenta os objetivos e cuidados arquiteturais.
Antes de adotar qualquer pacote, verifique manutenção, compatibilidade com a versão do Node.js, comportamento de timeouts, métricas disponíveis e política de erros. Evite depender dos valores padrão sem testar a carga real da aplicação.
Observabilidade
O estado do circuito precisa ser visível. Registre eventos de abertura, meia abertura e fechamento, mas evite gerar uma mensagem para cada requisição rejeitada em alto volume. Prefira métricas agregadas e logs amostrados.
Métricas importantes incluem estado atual, quantidade de transições, chamadas aceitas e rejeitadas, taxa de falhas, timeouts, duração das chamadas, uso do fallback e tempo em que o circuito permanece aberto.
Use nomes que identifiquem a dependência e a operação, como payment.authorize ou catalog.list. Um único circuito global para todas as rotas de um serviço pode bloquear operações saudáveis por causa de uma operação problemática.
Integração com OpenTelemetry
Adicione atributos aos spans para registrar o nome do circuito, estado e resultado. Quando uma chamada for rejeitada localmente, não a represente como se tivesse chegado ao serviço remoto. Crie um evento indicando que o circuit breaker bloqueou a tentativa.
Para rastreamento distribuído, consulte o guia de OpenTelemetry no Node.js.
Testando o circuit breaker
Evite testes que dependam de esperas reais de dezenas de segundos. Injete uma função de relógio ou use timers falsos. Os testes devem cobrir sucesso no estado fechado, abertura após atingir o limite, rejeição imediata enquanto aberto, transição para half-open, fechamento após um teste bem-sucedido, reabertura quando o teste falha, apenas uma chamada de teste simultânea e execução correta do fallback.
import assert from 'node:assert/strict';
let calls = 0;
const action = async () => {
calls += 1;
throw new Error('serviço indisponível');
};
const breaker = new CircuitBreaker(action, {
failureThreshold: 2,
resetTimeout: 10000
});
await assert.rejects(() => breaker.execute());
await assert.rejects(() => breaker.execute());
await assert.rejects(() => breaker.execute(), /Circuito aberto/);
assert.equal(calls, 2);Erros comuns
- Não usar timeout: chamadas lentas mantêm recursos ocupados antes de contar como falha.
- Abrir com poucas amostras: uma falha isolada bloqueia um serviço saudável.
- Contar erros do cliente: problemas de validação afetam indevidamente o circuito.
- Liberar todo o tráfego no half-open: a dependência recebe uma nova sobrecarga.
- Compartilhar um único circuito: operações independentes se bloqueiam mutuamente.
- Esconder o fallback: o consumidor acredita estar recebendo dados atuais.
- Usar retry ilimitado: a aplicação amplifica a falha externa.
- Não monitorar transições: o circuito abre frequentemente sem investigação.
Boas práticas para produção
- Configure timeout menor que o limite total da requisição.
- Use limites baseados em volume e taxa de falha.
- Separe circuitos por dependência e operação crítica.
- Restrinja a quantidade de probes no half-open.
- Combine poucas tentativas com backoff e jitter.
- Crie fallbacks transparentes e seguros.
- Exponha estado, latência e taxa de rejeição em métricas.
- Teste falhas reais em ambiente controlado.
- Revise os limites conforme o tráfego e o SLA evoluem.
- Não trate o circuit breaker como solução para erros de código.
Conclusão
O Circuit Breaker no Node.js protege a aplicação contra dependências lentas ou indisponíveis. Ao abrir o circuito depois de um padrão consistente de falhas, ele reduz espera, preserva conexões e impede que um problema externo se espalhe para todo o sistema.
Comece com timeout, contagem de falhas e três estados bem definidos. Depois, adicione janela estatística, controle de concorrência no half-open, métricas e fallbacks claros. Com testes e observabilidade, o padrão deixa de ser apenas uma barreira de emergência e se torna parte importante da resiliência da API.




