This page is also available in English.

Voltar aos artigos

O padrão Circuit Breaker

Publicado em 12 min de leitura

  • Distributed Systems
  • Resilience
  • Reliability
  • Microservices
Feixe de fios elétricos coloridos conectados a uma caixa de comutação

Foto de Mostafa Mahmoudi no Unsplash

Uma dependência que já está falhando não precisa de mais tráfego. Timeouts liberam threads. Retries dão uma segunda chance a falhas transitórias. Nenhum dos dois impede uma avalanche de clientes de martelar um serviço que só vai se recuperar em minutos. Esse é o papel do circuit breaker: falhar rápido no cliente, dar espaço para a dependência se recuperar e só então sondar com cuidado antes de restaurar o tráfego pleno.

Michael Nygard popularizou o padrão em Release It! (2007; 2ª ed. 2018). O texto clássico de Martin Fowler e a documentação do Azure Architecture Center da Microsoft formalizaram a máquina de três estados que a maioria das bibliotecas ainda oferece. Este artigo é uma referência prática: como os estados funcionam, como calibrar a detecção de falhas, como combinar breakers com retries e quando os builders da Amazon preferem uma cota de retry.

O que é

Um circuit breaker é um proxy em torno de uma chamada remota (ou de qualquer operação que possa falhar). Ele observa os resultados recentes. Quando as falhas cruzam um limiar, ele abre: novas chamadas devolvem erro (ou um fallback) sem invocar a dependência. Depois de um período de resfriamento, libera um número limitado de requisições de sonda. Sucesso fecha o circuito; falha abre de novo.

A metáfora elétrica é proposital. Um disjuntor abre sob corrente de falha para não destruir a fiação. Um breaker de software abre sob taxa de falha (ou falhas consecutivas) para não destruir seus pools de threads, conexões e chamadores upstream à espera de trabalho condenado.

Ele complementa — não substitui — timeouts, retries, jitter e backoff:

PreocupaçãoFerramenta
Limitar quanto uma chamada esperaTimeout / deadline
Esconder falhas curtas e rarasRetry com backoff + jitter
Limitar amplificação de retry em outageRetry budget / token bucket
Parar de chamar quando a dependência parece doenteCircuit breaker
Isolar domínios de falha (pools separados)Bulkhead

Como funciona

A descrição dos Cloud Design Patterns da Microsoft coincide com a máquina que quase toda biblioteca implementa.

Closed (fechado)

Operação normal. As requisições vão para a dependência. O breaker registra sucessos e falhas em uma janela (por contagem ou por tempo). Erros ocasionais permanecem em Closed. Quando o sinal de falha cruza o limiar configurado e já houve chamadas suficientes, o breaker passa para Open.

Open (aberto)

Falha rápida. As chamadas não chegam à dependência. O cliente recebe erro imediato (ou um fallback: cache, valor padrão, fila para depois). Um temporizador de espera / reset / break começa. Ao expirar, o breaker vai para Half-Open — não de volta direto a Closed.

Open é o ponto do padrão: você para de gastar latência e concorrência em trabalho que tende a falhar, e deixa de empurrar carga para um serviço em dificuldade.

Half-Open (meio aberto)

Recuperação cautelosa. Um número limitado de sondas é permitido. Se elas tiverem sucesso (por contagem ou por taxa de falha), o breaker volta a Closed e zera as métricas. Se uma sonda falha, volta a Open e o resfriamento recomeça.

Half-Open existe porque não dá para saber se houve recuperação sem tentar — mas inundar um serviço meio recuperado com tráfego pleno pode derrubá-lo de novo. A Microsoft destaca isso: uma dependência em recuperação pode só aguentar volume limitado até estar saudável.

Text
        falhas ≥ limiar
  Closed ───────────────────► Open
     ▲                         │
     │                         │ duração de espera expira
     │      sondas ok          ▼
     └─────────────── Half-Open

                         │ sonda falha
                         └──► Open

O que conta como falha?

Nem todo erro deve abrir o circuito. Fowler observa que algumas falhas são resultados de negócio normais e não devem abrir o breaker. Regras típicas em produção:

  • Contam como falha: timeouts, erros de conexão, HTTP 5xx, “dependência indisponível”
  • Em geral ignore: 4xx de validação / auth (culpa do chamador), “not found” esperado
  • Às vezes caso especial: HTTP 429 / 503 com Retry-After — a Microsoft descreve abertura acelerada: abrir imediatamente e permanecer aberto pelo menos o tempo que a dependência pediu para esperar

Bibliotecas também tratam chamadas lentas como sinal separado. O Resilience4j pode abrir o circuito quando a taxa de chamadas lentas passa do limiar (mais lentas que slowCallDurationThreshold), mesmo que eventualmente tenham sucesso — útil quando a latência, e não o erro duro, é o que esgota sua concorrência.

Detecção de falha na prática

Exemplos didáticos abrem após N falhas consecutivas. Breakers de produção costumam usar janela deslizante e taxa de falha.

Defaults do Resilience4j (bom modelo mental):

ConfiguraçãoDefaultPapel
slidingWindowTypeCOUNT_BASEDÚltimas N chamadas, ou últimos N segundos se for por tempo
slidingWindowSize100Largura da janela
minimumNumberOfCalls100Não avalia a taxa até existir esse volume
failureRateThreshold50Abre quando a % de falha ≥ isso
waitDurationInOpenState60sResfriamento antes do Half-Open
permittedNumberOfCallsInHalfOpenState10Orçamento de sondas

minimumNumberOfCalls evita que um processo frio abra após um ou dois erros azarados. A estratégia de circuit breaker do Polly usa a mesma ideia: razão de falha em uma duração de amostragem, com throughput mínimo antes de confiar na razão (exemplo da docs do Polly: 50% de falhas em qualquer janela de 10 segundos, pelo menos 8 ações, então abre por 30 segundos).

O opossum (Node.js) expõe os mesmos knobs com outros nomes: errorThresholdPercentage, volumeThreshold, resetTimeout, mais um timeout opcional por chamada.

Por que importa

Sem breaker, uma dependência lenta ou morta vira esgotamento de recursos no chamador: threads bloqueadas em timeout, pools de conexão presos, filas crescendo, e aí seus clientes dão timeout e fazem retry. O livro de SRE do Google sobre falhas em cascata descreve o risco central como feedback positivo — sobrecarga em uma réplica aumenta a carga nas demais até o tier inteiro cair. Proteção no cliente (falhar rápido, descartar trabalho, não retentar para sempre) é como cortar esse loop.

Breakers também melhoram a latência percebida. Um circuito aberto responde em microssegundos em vez de esperar um timeout de vários segundos. A documentação do Azure enfatiza que isso protege o tempo de resposta e dá um sinal claro para operações: transições de estado são ótimos eventos de saúde.

Quando usar (e quando não)

Use circuit breaker quando:

  • Chamadas síncronas a um serviço remoto ou recurso compartilhado podem falhar por minutos, não milissegundos
  • Retries sozinhos amplificariam carga durante um outage sustentado
  • Você precisa de degradação graciosa (cache, stub, fila, “tente depois”) enquanto a dependência está fora
  • Quer um modo explícito e observável de “paramos de chamar X” para ops

Prefira outra coisa quando:

  • As falhas são sobretudo curtas e raras — timeouts + retries limitados com jitter e um retry budget podem bastar
  • Vale o alerta da Amazon Builders’ Library: circuit breakers introduzem comportamento modal difícil de testar e podem atrasar a recuperação se mal calibrados. Eles costumam mitigar tempestades de retry com uma cota de retry em token bucket (no AWS SDK desde 2016)
  • Orientação da Microsoft: trabalho local em memória, exceções puramente de negócio, fluxos orientados a mensagens com dead-letter queue, ou isolamento na plataforma (mesh / health checks do load balancer) podem não precisar de breaker na aplicação
  • Esperar o resfriamento + sondas violaria um SLO duro de latência — falhe a requisição de outro modo e descarte carga upstream

A AWS Prescriptive Guidance ainda recomenda o padrão quando o callee está fora ou a latência alta geraria pilhas de retry, contenção de rede e consumo de thread pool.

Vantagens e trade-offs

Vantagens

  • Falha rápida em vez de filas de timeout
  • Alívio de carga para a dependência em dificuldade
  • Sinal claro para ops (Open / Half-Open)
  • Lugar natural para fallbacks e degradação

Trade-offs

  • Máquina de estados a mais para configurar, monitorar e testar (incluindo chamadores concorrentes)
  • Limiares ruins: Open falso (bate na disponibilidade) ou Open tardio (a cascata já começou)
  • Breaker compartilhado entre backends independentes (shards ou tenants) pode bloquear caminhos saudáveis — a Microsoft alerta contra misturar domínios de falha não relacionados
  • Half-Open agressivo demais → flapping na recuperação; tímido demais → degradação prolongada
  • Não corrige falta de timeout: timeouts longos da dependência ainda podem prender threads antes de o breaker registrar a falha

Exemplo prático

Um breaker mínimo em TypeScript no modelo clássico de três estados. Em produção, prefira uma biblioteca consolidada (opossum no Node, Polly no .NET, Resilience4j na JVM); este esboço mostra o fluxo de controle.

TypeScript
type State = "closed" | "open" | "half_open";

type Options = {
  failureThreshold: number; // falhas consecutivas para abrir (detector simples)
  resetTimeoutMs: number; // open → half-open
  successThreshold: number; // sucessos em half-open para fechar
};

class CircuitBreaker {
  private state: State = "closed";
  private failures = 0;
  private successes = 0;
  private openedAt = 0;

  constructor(private readonly options: Options) {}

  async exec<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === "open") {
      if (Date.now() - this.openedAt >= this.options.resetTimeoutMs) {
        this.state = "half_open";
        this.successes = 0;
      } else {
        throw new Error("CircuitOpen");
      }
    }

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

  private onSuccess() {
    if (this.state === "half_open") {
      this.successes += 1;
      if (this.successes >= this.options.successThreshold) {
        this.state = "closed";
        this.failures = 0;
      }
      return;
    }
    this.failures = 0;
  }

  private onFailure() {
    this.failures += 1;
    if (
      this.state === "half_open" ||
      this.failures >= this.options.failureThreshold
    ) {
      this.state = "open";
      this.openedAt = Date.now();
      this.successes = 0;
    }
  }
}

const payment = new CircuitBreaker({
  failureThreshold: 5,
  resetTimeoutMs: 30_000,
  successThreshold: 2,
});

// Em código real, combine com timeout por chamada
await payment.exec(() => chargeCard(orderId));

Com opossum, a mesma ideia vira configuração em vez de máquina de estados caseira:

TypeScript
import CircuitBreaker from "opossum";

const breaker = new CircuitBreaker(chargeCard, {
  timeout: 3_000,
  errorThresholdPercentage: 50,
  resetTimeout: 30_000,
  volumeThreshold: 10,
});

breaker.fallback(() => ({ status: "queued" })); // degrada enquanto open
breaker.on("open", () => console.warn("payment circuit open"));

await breaker.fire(orderId);

Combinando com retries

A regra da Microsoft é a que vale guardar: retente através de um breaker, mas pare de retentar quando o breaker indicar que a falha não é transitória (exceções de circuito aberto).

Um pipeline sensato:

  1. Timeout em cada tentativa
  2. Circuit breaker em torno da dependência
  3. Retry com full jitter só para erros retentáveis e só enquanto o circuito estiver fechado (ou na própria sonda)
  4. Retry budget para o processo inteiro não amplificar tráfego sem limite

Não retente erros de circuito aberto com a mesma agressividade de um 503 isolado. Isso recria a avalanche que o breaker acabou de parar.

Boas práticas

  1. Um breaker por domínio de falha — por dependência e, com frequência, por endpoint crítico ou shard, não um interruptor global do processo
  2. Exija volume mínimo antes de avaliar a taxa de falha
  3. Classifique erros — não abra por bug do chamador (400 / 401)
  4. Emite mudanças de estado — log e métrica em toda transição Closed → Open → Half-Open; alerte na duração de Open
  5. Ofereça fallbacks quando o produto permitir (cache velho, fila assíncrona, UI reduzida)
  6. Permita isolate / close manual — o CircuitBreakerManualControl do Polly e os estados forçados do Resilience4j existem para ops drenarem tráfego em incidentes
  7. Teste os modos sob carga — caminho feliz em Closed, fail-fast em Open, limites de sonda em Half-Open e recuperação com tráfego subindo
  8. Calibre o resfriamento à realidade da recuperação — failover de banco e caches frios pedem Open mais longo do que uma única réplica sobrecarregada

Erros comuns

  • Breaker sem timeouts — threads ainda se acumulam esperando a primeira amostra de falha
  • Retries ignorando circuito aberto — multiplica a carga que o breaker tentou cortar
  • Um breaker para muitos backends — um shard ruim apaga shards saudáveis
  • Limiar baixo demais / sem mínimo de chamadas — flapping em tráfego esparso
  • Limiar alto demais / janela larga demais — a cascata termina antes do Open
  • Half-Open sem limite — o Hystrix clássico usava uma sonda; libs modernas permitem um conjunto pequeno — mantenha pequeno
  • Tratar o breaker como regra de negócio — é controle de carga e falha, não substituto de tratamento de domínio
  • Sem história de fallback — Open sem resposta de produto é só um 500 mais rápido

Alternativas e comparações

AbordagemO que fazEm relação ao breaker
Retry budget / token bucketLimita retries no processoSuperfície modal mais simples; a Amazon muitas vezes prefere isso para amplificação de retry
Adaptive throttling no cliente (Google SRE)Descarta probabilisticamente quando rejeições sobemThrottle contínuo vs Open/Closed binário
BulkheadPools / limites de concorrência por dependênciaIsola o raio de explosão; costuma ir junto com breakers
Load sheddingServidor recusa excesso de trabalhoProtege o callee; o breaker protege o caller
Breakers em mesh / gatewayPolítica fora do código da appCentralizado, mas ainda precisa de domínios e métricas corretos
Hedged requestsTentativa paralela especulativaProblema diferente (latência de cauda); pode aumentar carga

Use breakers quando precisar de um stop duro e de um handshake explícito de recuperação. Use budgets e shedding quando o objetivo for sobretudo limitar amplificação sem uma mudança de modo abrupta.

Perguntas frequentes

Circuit breaker é a mesma coisa que timeout?

Não. Timeout limita uma chamada. O breaker decide se tenta a chamada com base na saúde recente.

Toda chamada entre microserviços precisa de circuit breaker?

Não. Comece com deadlines, retries idempotentes, jitter e um retry budget. Adicione breaker onde falha sustentada da dependência cascatearia ou prenderia recursos — tipicamente caminhos síncronos críticos.

Onde o breaker deve viver — cliente, biblioteca ou mesh?

Onde você puder observar resultados e falhar rápido. Bibliotecas in-process (opossum, Polly, Resilience4j) são fáceis de raciocinar por serviço. Meshes e gateways ajudam quando várias linguagens compartilham o mesmo plano de políticas. Estado compartilhado distribuído (por exemplo status em DynamoDB nos samples da AWS) ajuda quando muitas instâncias precisam da mesma visão de “pagamento está fora”.

O que o cliente deve fazer com o circuito aberto?

Trate como outage conhecido: degrade, enfileire trabalho, mostre UI clara ou falhe a jornada do usuário rápido. Não fique em loop de retry contra CircuitOpen / BrokenCircuitException.

Como escolher taxa de falha e tempo de resfriamento?

Parta dos SLOs e dos tempos de recuperação da dependência. Se p99 e error budget dizem que o serviço está doente acima de ~X% de erros por Y segundos, alinhe a janela e o limiar por perto — e valide sob carga. Prefira throughput mínimo conservador para tráfego esparso não fazer o breaker bater.

Conclusão

Um circuit breaker é como você para de chamar uma dependência que deixou de ser útil. Closed monitora, Open protege, Half-Open verifica. Combine com timeouts e retries cuidadosos; respeite erros de circuito aberto; isole domínios de falha; e meça transições de estado como qualquer outro plano de controle em produção.

Se você só precisa domar tempestades de retry, experimente um retry budget primeiro. Se precisa de um stop duro e degradação graciosa enquanto algo fica fora por minutos, instale um breaker — e teste os caminhos Open e Half-Open com o mesmo cuidado do caminho feliz.

Referências

  1. Circuit Breaker — Martin Fowler (bliki; credita Michael Nygard, Release It!)
  2. Circuit Breaker pattern — Microsoft Azure Architecture Center (atualizado em 2025)
  3. Circuit breaker pattern — AWS Prescriptive Guidance
  4. Timeouts, retries, and backoff with jitter — Amazon Builders’ Library (Marc Brooker; trade-offs do circuit breaker e cotas de retry em token bucket)
  5. Addressing Cascading Failures — Google SRE Book, Capítulo 22
  6. Handling Overload — Google SRE Book (throttling adaptativo no cliente)
  7. CircuitBreaker — Documentação Resilience4j (defaults de sliding window)
  8. Circuit breaker resilience strategy — Documentação Polly
  9. opossum — Circuit breaker para Node.js (nodeshift)
  10. Implement the Circuit Breaker pattern — Guia .NET de microserviços (Polly + IHttpClientFactory)
  11. Nygard, Michael. Release It! Design and Deploy Production-Ready Software — Pragmatic Bookshelf (1ª ed. 2007; 2ª ed. 2018)

Comentários