Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Circuit Breaker no Node.js

Atualizado em: 30 de setembro de 2026

Rack de servidores processando fluxos de dados no Node.js

Circuit breaker é um padrão de resiliência que interrompe temporariamente chamadas a uma dependência que está falhando ou respondendo lentamente. Em vez de continuar enviando requisições destinadas ao timeout, a aplicação falha rapidamente, preserva threads, sockets, conexões e capacidade para operações saudáveis.

O padrão é inspirado em um disjuntor elétrico e possui estados como fechado, aberto e meio aberto. Ele deve ser combinado com timeouts, limites de concorrência, retries controlados, fallback e observabilidade.

Problema que resolve

Quando um serviço externo fica lento, cada chamada pode ocupar recursos por segundos. Novas requisições se acumulam, pools esgotam e o event loop passa a processar timeouts e callbacks em grande volume. A falha de uma dependência se transforma em falha em cascata.

O circuit breaker detecta uma taxa de erro ou lentidão e abre o circuito, rejeitando novas chamadas por um período.

Estados

  • closed: chamadas passam e resultados são medidos;
  • open: chamadas são rejeitadas imediatamente;
  • half-open: poucas chamadas de teste verificam recuperação.

Se a chamada de teste funciona, o circuito volta a closed. Se falha, retorna a open.

Implementação didática

class CircuitBreaker {
  constructor({ failureThreshold = 5, resetTimeoutMs = 10_000 }) {
    this.failureThreshold = failureThreshold;
    this.resetTimeoutMs = resetTimeoutMs;
    this.failures = 0;
    this.state = 'closed';
    this.openedAt = 0;
  }

  async execute(operation) {
    if (this.state === 'open') {
      const elapsed = Date.now() - this.openedAt;
      if (elapsed < this.resetTimeoutMs) {
        throw new Error('Circuito aberto');
      }
      this.state = 'half-open';
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }

  onFailure() {
    this.failures += 1;
    if (this.state === 'half-open' || this.failures >= this.failureThreshold) {
      this.state = 'open';
      this.openedAt = Date.now();
    }
  }
}

Esse exemplo ensina o fluxo, mas não possui janela de métricas, concorrência half-open, classificação de erro, timeout, fallback ou proteção distribuída. Em produção, use uma biblioteca madura ou implementação testada.

Timeout vem antes

O breaker só recebe um resultado quando a operação conclui ou falha. Sem timeout, uma chamada pode ficar pendurada:

async function callInventory(productId, signal) {
  const timeoutSignal = AbortSignal.timeout(1500);
  const combined = AbortSignal.any([signal, timeoutSignal]);

  const response = await fetch(
    `https://inventory.internal/products/${productId}`,
    { signal: combined },
  );

  if (!response.ok) {
    throw new Error(`Inventory HTTP ${response.status}`);
  }

  return response.json();
}

O timeout deve ser menor que o prazo da requisição chamadora e compatível com o SLO da dependência.

Quais erros contar

Nem toda falha indica indisponibilidade:

  • timeout e erro de conexão: normalmente contam;
  • HTTP 500, 502, 503 e 504: normalmente contam;
  • HTTP 429: pode contar como sobrecarga, mas exige Retry-After;
  • HTTP 400 por entrada inválida: não deveria abrir o circuito;
  • HTTP 404 esperado: normalmente não conta;
  • cancelamento do cliente: não é falha da dependência;
  • erro de programação local: deve ser tratado separadamente.

Implemente uma função de classificação.

Taxa de erro em janela

Um número consecutivo de falhas é simples, mas instável. Um modelo melhor usa janela de volume:

abrir quando:
- houver pelo menos 20 chamadas na janela
- e taxa de erro for maior ou igual a 50%
- ou taxa de chamadas lentas exceder o limite

O volume mínimo impede que uma única falha abra um serviço de baixo tráfego.

Chamadas lentas

Uma dependência pode retornar 200 depois de quatro segundos e ainda causar colapso. Meça duração e considere chamadas lentas no breaker. O limiar deve refletir o orçamento de latência.

Half-open controlado

Quando o reset timeout termina, não libere todo o tráfego. Permita uma ou poucas requisições de teste. Caso contrário, milhares de chamadas podem atingir a dependência simultaneamente e derrubá-la novamente.

Fallback

Fallback precisa preservar semântica e segurança. Exemplos:

  • cache recente;
  • resposta parcial;
  • lista vazia para recomendação opcional;
  • fila para processamento posterior;
  • mensagem de indisponibilidade;
  • valor padrão explicitamente marcado.

Não use fallback que inventa saldo, preço, autorização ou confirmação financeira.

async function getRecommendations(userId) {
  try {
    return await recommendationsBreaker.execute(() =>
      recommendationsApi.get(userId),
    );
  } catch (error) {
    logger.warn({ error, userId }, 'Recomendações indisponíveis');
    return [];
  }
}

Evite registrar user ID sem política de privacidade; use identificadores apropriados.

Retry e circuit breaker

Retries aumentam carga em uma dependência ruim. Ordem típica:

  1. aplicar timeout por tentativa;
  2. tentar novamente apenas erros transitórios;
  3. usar backoff com jitter;
  4. limitar tentativas pelo deadline;
  5. registrar o resultado final no breaker.

Algumas bibliotecas contam cada tentativa; outras contam a operação inteira. Entenda a configuração para não abrir cedo demais.

Backoff com jitter

function retryDelay(attempt, baseMs = 100, maxMs = 2000) {
  const cap = Math.min(maxMs, baseMs * 2 ** attempt);
  return Math.random() * cap;
}

Jitter evita que muitas instâncias repitam ao mesmo tempo.

Bulkhead

Circuit breaker reage a falhas; bulkhead limita quantas chamadas podem estar em andamento. Combine:

class Semaphore {
  constructor(limit) {
    this.limit = limit;
    this.active = 0;
    this.queue = [];
  }

  async acquire() {
    if (this.active < this.limit) {
      this.active += 1;
      return;
    }
    await new Promise((resolve) => this.queue.push(resolve));
    this.active += 1;
  }

  release() {
    this.active -= 1;
    this.queue.shift()?.();
  }
}

Limite também a fila. Uma fila infinita apenas troca saturação de sockets por saturação de memória.

Breaker por dependência e operação

Um único circuito para toda a API externa pode desligar operações saudáveis por causa de um endpoint ruim. Por outro lado, um breaker por usuário ou URL gera milhares de estados.

Escolha uma granularidade estável, como:

inventory:read
inventory:reserve
payments:authorize
payments:refund

Estado local ou distribuído

Normalmente cada instância mantém seu próprio breaker. Isso reduz dependência e permite reação rápida. O estado pode diferir entre pods, mas todos observam falhas semelhantes.

Compartilhar estado em Redis adiciona latência e cria uma nova dependência. Só faça quando houver necessidade clara. Métricas agregadas fornecem visão global sem coordenar cada decisão.

Cold start

Uma instância nova começa com circuito fechado e pode atingir uma dependência já falhando. Você pode usar configuração operacional, service mesh ou limites de concorrência para reduzir impacto. Não persista indefinidamente um circuito aberto sem testar recuperação.

Opossum

O ecossistema Node.js possui bibliotecas como Opossum. Um uso típico:

import CircuitBreaker from 'opossum';

const breaker = new CircuitBreaker(callInventory, {
  timeout: 1500,
  errorThresholdPercentage: 50,
  resetTimeout: 10_000,
  volumeThreshold: 20,
});

breaker.fallback(() => ({ available: null, source: 'fallback' }));

Confira a versão, opções e manutenção antes de adotar. Teste o comportamento de timeout, erros e shutdown.

Eventos do breaker

breaker.on('open', () => logger.warn('Circuito aberto'));
breaker.on('halfOpen', () => logger.info('Circuito meio aberto'));
breaker.on('close', () => logger.info('Circuito fechado'));
breaker.on('fallback', () => metrics.fallbacks.inc());

Evite log por cada rejeição quando o circuito está aberto. Agregue métricas e faça amostragem.

Métricas

Exponha:

  • estado do circuito;
  • transições;
  • chamadas permitidas;
  • rejeitadas por open;
  • sucessos e falhas;
  • timeouts;
  • chamadas lentas;
  • fallbacks;
  • tentativas half-open;
  • concorrência e fila.

Use labels de baixa cardinalidade: serviço, operação, ambiente e versão.

Alertas

Um circuito aberto por segundos pode ser proteção normal. Alerte quando:

  • permanece aberto por período sustentado;
  • muitas operações estão em fallback;
  • vários pods abrem simultaneamente;
  • o SLO de usuário é afetado;
  • half-open falha repetidamente;
  • a fila ou rejeições crescem.

Health checks

Não marque a aplicação inteira como not-ready apenas porque um breaker opcional abriu. Isso removeria capacidade e poderia causar efeito dominó. Readiness deve considerar se o serviço ainda cumpre sua função principal.

Um endpoint operacional autenticado pode mostrar estados dos breakers para diagnóstico.

Graceful shutdown

No shutdown, pare de aceitar novas operações, aguarde chamadas em andamento dentro do prazo e encerre timers da biblioteca. Um breaker não substitui cancelamento global.

AbortController

Propague o signal da requisição pelo breaker e pelas tentativas. Se o cliente desconectar, não continue consumindo quota e dependências.

Cache como fallback

Defina:

  • idade máxima;
  • indicação de dado stale;
  • quais operações podem usar cache;
  • proteção contra stampede na recuperação;
  • estratégia quando cache também falha.

Use stale-while-revalidate apenas quando a semântica permitir.

Fila como fallback

Para operação assíncrona, enfileirar pode preservar intenção:

try {
  await breaker.execute(() => sendNotification(payload));
} catch (error) {
  await durableQueue.add('notification', payload, {
    jobId: idempotencyKey,
  });
}

A fila deve ser durável, limitada e observada. Não enfileire indefinidamente uma operação expirada.

Idempotência

Retries e filas podem repetir operações. Para pagamentos, reservas e criação de recursos, use chave de idempotência e contrato da dependência.

Service mesh

Gateways e service meshes podem implementar circuit breaking, limites e retries fora da aplicação. Isso padroniza infraestrutura, mas a aplicação ainda conhece semântica, fallback e idempotência. Evite duas camadas com retries multiplicativos.

Testes

Teste:

  • sucessos normais;
  • erros consecutivos;
  • taxa de erro na janela;
  • timeout;
  • erro que não deve contar;
  • abertura;
  • rejeição rápida;
  • half-open concorrente;
  • fechamento após recuperação;
  • fallback;
  • shutdown;
  • múltiplas instâncias.

Chaos testing

Simule latência, reset de conexão, HTTP 503, 429 e respostas parciais. Verifique que o breaker protege recursos e que a aplicação se recupera sem intervenção manual.

Teste de carga

Compare com breaker fechado, dependência lenta e circuito aberto. Quando aberto, a latência deve cair e a fila não deve crescer, mas a taxa de fallback aumenta. Monitore CPU, memória, sockets e p99.

Erros comuns

  • usar breaker sem timeout;
  • contar erro 400 como falha da dependência;
  • liberar todo tráfego no half-open;
  • aplicar retry agressivo;
  • usar fallback incorreto;
  • criar breaker por usuário;
  • não limitar concorrência;
  • abrir com uma única falha em baixo volume;
  • marcar serviço inteiro not-ready;
  • não medir transições e rejeições.

Fluxo recomendado

Comece com timeout e limite de concorrência, classifique falhas, escolha janela e volume mínimo, defina half-open controlado e fallback seguro. Monitore estados e teste recuperação. Combine com AbortController, Rate Limiting, Health Checks, Graceful Shutdown e métricas em Prometheus.

Consulte a documentação do padrão Circuit Breaker e o repositório oficial do Opossum.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita