O disjuntor do quadro de luz
Quando há curto-circuito, o disjuntor desarma. Ele não conserta o curto — ele impede que a corrente continue passando e queime a fiação inteira. E ninguém religa sem parar: você espera, religa uma vez para testar, e se desarmar de novo, sabe que o problema continua lá. O disjuntor de software faz exatamente isso, contando falhas em vez de amperes.
Insistir custa dos dois lados
Chamar uma dependência que está fora não é neutro. Cada tentativa gasta uma conexão sua, consome o orçamento de tempo do pedido, e termina no mesmo erro — mas trinta segundos depois, em vez de imediatamente.
E do outro lado é pior: um serviço que caiu por sobrecarga precisa de folga para se recuperar, e a insistência dos clientes é justamente o que impede isso.
O padrão resolve dois problemas que parecem um só. O primeiro é proteger quem chama: sem disjuntor, cada requisição paga o timeout inteiro para descobrir algo que já se sabia. Com trinta segundos de prazo e uma dependência morta, o seu pool esgota do mesmo jeito que esgotaria sem timeout nenhum — só um pouco mais devagar.
O segundo é proteger quem é chamado. Um serviço sobrecarregado que recebe menos tráfego consegue drenar a fila e voltar. Um serviço sobrecarregado que recebe o mesmo tráfego mais os retries nunca sai do buraco. O disjuntor é, do ponto de vista da dependência, um mecanismo de alívio de carga imposto pelos clientes.
Vale distinguir de Retry, que é o padrão irmão e resolve o problema oposto. Retry aposta que a falha é passageira e que insistir vale a pena. Circuit Breaker aposta que a falha é persistente e que insistir custa caro. Os dois convivem: repete-se algumas vezes; se o padrão de falha se mantém, o disjuntor abre e as repetições param. Sem o disjuntor, o retry sozinho vira o *thundering herd*.
Quem chamanão sabe que existe disjuntor no caminho
Disjuntorconta falhas e decide se deixa passar
Fechadopassa tudo, contando
Abertorecusa na hora, sem chamar
Meio-abertodeixa poucas passarem, em teste
Caminho degradadocache antigo, valor padrão, seção escondida
Dependência externapode estar fora por minutos
Três estados, e o do meio é o que importa
Fechado é a operação normal: tudo passa, e as falhas vão sendo contadas. Aberto é a recusa imediata, sem tocar na dependência. Meio-aberto é o estado de teste, e é onde mora a sutileza.
Ao sair do aberto, o disjuntor não pode simplesmente voltar a passar tudo — isso jogaria o tráfego acumulado de uma vez sobre um serviço que talvez mal tenha se levantado.
Meio-aberto deixa passar poucas requisições, em teste. Se elas tiverem sucesso, o circuito fecha; se uma falhar, ele volta a abrir imediatamente — não espera atingir limiar de novo, porque a evidência de que ainda está ruim já apareceu.
O erro clássico é dimensionar o limiar por contagem absoluta em vez de taxa. "Abre depois de 5 falhas" se comporta de maneira completamente diferente a 10 requisições por segundo e a 10 mil: no segundo caso, 5 falhas é ruído estatístico normal. O limiar sensato é uma fração — e com um mínimo de amostras, para não abrir com uma falha em uma requisição.
O disjuntor também precisa ser por dependência, não global. Um disjuntor único para todas as chamadas externas abre por causa do serviço de recomendação e derruba junto o de pagamento, que estava perfeito. E, em geral, por dependência *e por instância*: é assim que o *outlier detection* de um service mesh funciona.
Como isso aparece na prática
O gateway de pagamento que ficou fora por 20 minutos
Cenário
O provedor de pagamento tem um incidente e passa a responder 503 em 100% das chamadas. Sem disjuntor, cada tentativa de compra espera o timeout de 10 segundos antes de falhar, e o pool de conexões do checkout esgota.
Como o padrão entra
O disjuntor abre após 50% de falha em 20 amostras e passa a recusar em microssegundos. A interface mostra 'pagamento temporariamente indisponível, tente em instantes' de imediato, e oferece o boleto como alternativa.
Trade-off: Enquanto aberto, mesmo uma recuperação parcial do provedor não é aproveitada até a próxima sondagem — perde-se alguns minutos de vendas possíveis em troca de o resto do site continuar de pé.
A busca que virou opcional
Cenário
O cluster de busca fica degradado sob carga de pico e responde em 15 segundos. A home usa a busca para montar um bloco de sugestões, e a página inteira passa a demorar 15 segundos.
Como o padrão entra
Disjuntor com limiar baixo e caminho degradado: com o circuito aberto, o bloco de sugestões cai para uma lista estática pré-computada, e a home volta a carregar em 200ms.
Trade-off: As sugestões ficam genéricas durante o incidente, o que provavelmente reduz conversão naquele bloco. É uma perda mensurável e aceita — bem menor que a de uma home que não carrega.
Onde isto dá errado
Limiar por contagem absoluta em vez de taxa
"Abre depois de 5 falhas" significa coisas opostas a 10 e a 10 mil requisições por segundo. No volume alto, 5 falhas é ruído normal e o circuito abre sem motivo. O limiar precisa ser uma fração das amostras, com um mínimo antes de julgar.
Um disjuntor global para todas as dependências
Se o mesmo disjuntor cobre recomendação, busca e pagamento, o serviço menos importante derruba o mais crítico ao abrir o circuito de todos. O escopo correto é por dependência — e, quando possível, por instância.
Janela de espera curta demais
Sondar de cinco em cinco segundos um serviço que leva minutos para voltar mantém uma pressão constante sobre quem está tentando se levantar, e nunca dá tempo de a recuperação acontecer. A espera precisa ser da ordem do tempo real de recuperação.
Voltar de meio-aberto para fechado de uma vez
Ao sair do estado aberto, liberar todo o tráfego acumulado de uma vez derruba de novo o serviço que mal tinha se levantado. Meio-aberto existe para deixar passar poucas requisições em teste, e uma falha nelas basta para reabrir.
Abrir o circuito sem ter para onde ir
Se a dependência é essencial e não há caminho degradado, abrir o circuito só troca um erro lento por um erro rápido. Continua sendo melhor — o prazo é devolvido —, mas o ganho é bem menor do que se imagina, e não justifica sozinho a complexidade.
Contar timeout do cliente como falha da dependência
Se o seu timeout está apertado demais, você registra falha em chamadas que teriam funcionado. O disjuntor abre por causa da sua configuração, não da saúde do outro lado — e o diagnóstico aponta para o lugar errado.
A máquina de estados
Um disjuntor com o relógio injetado
type Estado = "fechado" | "aberto" | "meio-aberto";
interface Config {
/** fracao de falhas que abre o disjuntor (ex.: 0.5 = 50%). */
limiar: number;
/** minimo de amostras antes de julgar — evita abrir com 1 de 1. */
minimoAmostras: number;
/** quanto tempo fica aberto antes de sondar. */
esperaMs: number;
}
class Disjuntor {
private estado: Estado = "fechado";
private falhas = 0;
private total = 0;
private abertoDesde = 0;
// 'agora' entra por parametro: nada de Date.now() aqui dentro.
permitir(agora: number): boolean {
if (this.estado === "aberto") {
if (agora - this.abertoDesde < this.cfg.esperaMs) return false;
this.estado = "meio-aberto"; // hora de sondar
}
return true;
}
registrar(ok: boolean, agora: number): void {
if (this.estado === "meio-aberto") {
// Em teste: um erro basta para voltar a abrir.
if (ok) this.fechar();
else this.abrir(agora);
return;
}
this.total++;
if (!ok) this.falhas++;
if (this.total >= this.cfg.minimoAmostras &&
this.falhas / this.total > this.cfg.limiar) {
this.abrir(agora);
}
}
constructor(private cfg: Config) {}
private abrir(agora: number) { this.estado = "aberto"; this.abertoDesde = agora; }
private fechar() { this.estado = "fechado"; this.falhas = 0; this.total = 0; }
}from dataclasses import dataclass
@dataclass
class Config:
limiar: float = 0.5 # fracao de falhas que abre
minimo_amostras: int = 20 # nao julga com poucos dados
espera_s: float = 30.0 # quanto fica aberto antes de sondar
class Aberto(Exception):
"""Recusado sem chamar a dependencia — a falha vira imediata e barata."""
class Disjuntor:
def __init__(self, cfg: Config):
self.cfg, self.estado = cfg, "fechado"
self.falhas = self.total = 0
self.aberto_desde = 0.0
def chamar(self, fn, agora: float):
if self.estado == "aberto":
if agora - self.aberto_desde < self.cfg.espera_s:
raise Aberto() # falha rapida: nao gasta prazo nem conexao
self.estado = "meio-aberto" # deixa UMA passar, em teste
try:
r = fn()
except Exception:
self._registrar(False, agora)
raise
self._registrar(True, agora)
return rQuando usar × quando evitar
Use quando
3- Dependências externas que podem ficar fora por minutos.
- Quando existe caminho degradado aceitável.
- Junto com timeout e retry — os três se completam.
Evite quando
3- Em volume baixo, onde a taxa de falha é ruído estatístico.
- Como substituto de timeout: sem prazo não há falha para contar.
- Em dependência essencial e sem alternativa, onde o ganho é pequeno.
Testar o que ficou
6 armadilhas de Circuit Breaker viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Outlier detection do EnvoyO service mesh tira do balanceamento a instância que passa a falhar, e a devolve depois de um tempo.Health check de load balancerParar de mandar tráfego para quem falhou nas últimas sondagens é o mesmo padrão, no nível do balanceador.O disjuntor do seu quadro de luzO nome é emprestado dele: corta o circuito antes que a sobrecarga queime a fiação inteira.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Ja falhou demais? Nem tenta.
if (!disjuntor.permitir(agora)) throw new Aberto();O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Três parâmetros para calibrar (limiar, amostras mínimas, janela de espera)
- Um estado a mais para observar e entender durante o incidente
Não vale se: não há caminho degradado. Sem alternativa, abrir o circuito só troca um erro lento por um rápido.
Onde isto já custou caro
Incidentes reais que este conceito explica.
- Amazon Web ServicesUm comando digitado errado tira metade da internet do arOs serviços que dependiam do S3 continuaram insistindo durante horas, gastando o próprio prazo em chamadas garantidamente perdidas — e amplificando a carga sobre quem tentava se levantar.
- Knight Capital GroupUS$ 440 milhões em 45 minutos por uma flag de código mortoNão havia nada observando o comportamento anômalo para interromper automaticamente. Quarenta e cinco minutos é muito tempo para um sistema perceber que está fazendo algo absurdo.
Onde isto aparece
Continue por aqui
- Retry com backoff e jitterFalhas passageiras se resolvem sozinhas se você esperar um pouco e tentar de novo. Mas repetir do jeito errado — sem espera crescente, sem ruído aleatório, sem filtrar o que é repetível — transforma um soluço em uma queda.
- TimeoutTodo pedido que sai da sua máquina precisa de um prazo máximo. Sem ele, a falha mais comum em produção não é o erro — é a espera infinita, que consome recursos até derrubar quem ainda estava saudável.
- BulkheadRecursos compartilhados propagam falha: se todas as chamadas disputam o mesmo pool, a dependência mais lenta consome tudo e derruba as rotas que nem falavam com ela. O anteparo divide o recurso em compartimentos com teto próprio.