This page is also available in English.

Publicado em 12 min de leitura

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.
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ção | Ferramenta |
|---|---|
| Limitar quanto uma chamada espera | Timeout / deadline |
| Esconder falhas curtas e raras | Retry com backoff + jitter |
| Limitar amplificação de retry em outage | Retry budget / token bucket |
| Parar de chamar quando a dependência parece doente | Circuit breaker |
| Isolar domínios de falha (pools separados) | Bulkhead |
A descrição dos Cloud Design Patterns da Microsoft coincide com a máquina que quase toda biblioteca implementa.
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.
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.
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.
falhas ≥ limiar
Closed ───────────────────► Open
▲ │
│ │ duração de espera expira
│ sondas ok ▼
└─────────────── Half-Open
│
│ sonda falha
└──► OpenNem 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:
5xx, “dependência indisponível”4xx de validação / auth (culpa do chamador), “not found” esperado429 / 503 com Retry-After — a Microsoft descreve abertura acelerada: abrir imediatamente e permanecer aberto pelo menos o tempo que a dependência pediu para esperarBibliotecas 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.
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ção | Default | Papel |
|---|---|---|
slidingWindowType | COUNT_BASED | Últimas N chamadas, ou últimos N segundos se for por tempo |
slidingWindowSize | 100 | Largura da janela |
minimumNumberOfCalls | 100 | Não avalia a taxa até existir esse volume |
failureRateThreshold | 50 | Abre quando a % de falha ≥ isso |
waitDurationInOpenState | 60s | Resfriamento antes do Half-Open |
permittedNumberOfCallsInHalfOpenState | 10 | Orç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.
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.
Use circuit breaker quando:
Prefira outra coisa quando:
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
Trade-offs
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.
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:
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);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:
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.
400 / 401)CircuitBreakerManualControl do Polly e os estados forçados do Resilience4j existem para ops drenarem tráfego em incidentes| Abordagem | O que faz | Em relação ao breaker |
|---|---|---|
| Retry budget / token bucket | Limita retries no processo | Superfí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 sobem | Throttle contínuo vs Open/Closed binário |
| Bulkhead | Pools / limites de concorrência por dependência | Isola o raio de explosão; costuma ir junto com breakers |
| Load shedding | Servidor recusa excesso de trabalho | Protege o callee; o breaker protege o caller |
| Breakers em mesh / gateway | Política fora do código da app | Centralizado, mas ainda precisa de domínios e métricas corretos |
| Hedged requests | Tentativa paralela especulativa | Problema 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.
Não. Timeout limita uma chamada. O breaker decide se tenta a chamada com base na saúde recente.
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 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”.
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.
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.
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.
IHttpClientFactory)