Circuit breakers e isolamento de falhas para APIs em produção
Uma dependência lenta não falha — enfileira. **Isolamento de falhas com circuit breaker** impede que um upstream doente consuma cada thread, conexão e orçamento de retentativas da frota. O padrão funciona quando limiares, fallbacks e probes de recuperação são explícitos antes do outage.
O provedor de pagamentos retornou HTTP 503 por quarenta segundos. O serviço de checkout retentou três vezes por request. Cada retentativa manteve uma conexão por trinta segundos. Pools de conexão saturaram. Endpoints de leitura não relacionados deram timeout porque cada worker ficou bloqueado esperando um vendor que ninguém podia consertar de dentro da aplicação. A dependência não derrubou o serviço — ocupou ele. Isolamento de falhas com circuit breaker existe para converter falha de dependência de um stall em toda a frota em uma superfície de fast-fail delimitada com fallback definido.
Um circuit breaker é uma máquina de estados envolvendo chamadas de saída. Enquanto saudável, chamadas passam (closed). Após falhas ou respostas lentas suficientes, o breaker abre e rejeita chamadas novas imediatamente — sem espera de socket, sem tempestade de retentativas. Após cooldown, um gotejo de probes half-open testa recuperação antes de fechar de novo. O padrão tem décadas. Implementações em produção ainda falham porque equipes tratam a biblioteca como arquitetura em vez de definir o que significa falha, o que retornar quando o circuito abre e quem recebe página quando probes half-open continuam falhando.
Isolamento de falhas começa com limites de blast radius
Isolamento de falhas traça uma linha em torno do que um outage de dependência pode afetar. Sem essa linha, retentativas propagam carga de volta ao caller, thread pools esgotam e health checks falham em serviços que nunca chamaram a dependência quebrada diretamente.
| Isolation layer | What it protects | Typical failure mode without it |
|---|---|---|
| Process / container | CPU and memory on one host | Noisy neighbor within the pod |
| Connection pool limits | Threads and sockets | Pool starvation from one slow peer |
| Circuit breaker | Latency tail and error budget | Retry amplification across the fleet |
| Bulkhead (separate pools) | Critical vs non-critical paths | Checkout blocked by analytics export |
| Timeout + deadline | End-to-end request budget | Unbounded waits that never surface as errors |
Circuit breakers ficam entre timeouts e bulkheads. Timeouts limitam quanto uma chamada pode esperar. Breakers limitam quantas chamadas podem esperar de fato quando a dependência está claramente unhealthy. Bulkheads garantem que abertura do circuito de analytics não drene o pool usado para autenticação.
Circuit breaker sem fallback é um 503 mais rápido. Fallback sem circuit breaker é um mais lento.
Definir o fallback antes do tráfego de produção atingir a dependência: resposta em cache, feature degradada, job async enfileirado ou erro explícito com mensagem visível ao usuário. "Retornar null e esperar que upstream lide" não é fallback — é contrato não documentado que vira incidente de integridade de dados no próximo deploy.
Estados do circuit breaker e os limiares que importam
A máquina de estados é simples. O tuning não.
Closed — todas as chamadas executam. Contadores registram falhas, respostas lentas e opcionalmente erros consecutivos. Um único 500 de endpoint flaky não deve abrir o circuito; falha sustentada sim.
Open — chamadas falham rápido sem I/O de rede. O caller retorna fallback ou erro imediatamente. Nenhuma thread bloqueia. Nenhuma retentativa dispara contra peer conhecido como doente.
Half-open — após janela de sleep, uma ou poucas requests de probe testam a dependência. Sucessos fecham o circuito; falhas reabrem e resetam a janela de sleep.
Design de limiares separa breakers úteis de barulhentos:
failure_rate_threshold = failures / (failures + successes) over rolling window
slow_call_threshold = responses slower than T ms count as failures
minimum_calls = do not evaluate until N requests in window (avoid opening on 1/1)
wait_duration_open = how long to stay open before half-open probes
permitted_half_open = max concurrent probes in half-open state
Pontos de partida concretos para dependências HTTP (ajustar com baselines medidos):
| Dependency class | Failure threshold | Slow threshold | Open duration |
|---|---|---|---|
| Payment / auth | 50% over 20 requests | p95 > 2 s | 30–60 s |
| Internal gRPC | 30% over 50 requests | p95 > 500 ms | 15–30 s |
| Best-effort enrichment | 60% over 10 requests | p95 > 1 s | 10 s |
Janelas deslizantes superam contagens fixas para tráfego em rajadas. Breakers por dependência superam breaker global — breaker global transforma outage de busca em outage de login.
Instrumentar transições de estado: circuit_opened, circuit_half_open, circuit_closed, etiquetados por nome de dependência e serviço caller. Esses eventos pertencem ao mesmo dashboard que SLIs and SLOs without an SRE team — circuito preso aberto vinte minutos é evento de confiabilidade visível ao usuário mesmo que taxas de erro pareçam aceitáveis porque fallbacks mascaram a falha.
Retentativas e circuit breakers devem ser coordenados
Retentativas multiplicam carga. Circuit breaker existe em parte para parar multiplicação de retentativas contra dependência que não pode absorvê-las.
Anti-padrão: retentar três vezes com backoff exponencial, depois envolver todo o loop de retentativas em breaker que só vê a falha final. A dependência recebe três vezes o tráfego enquanto o breaker acha que a taxa de falha é baixa porque o cliente absorveu dois sucessos-antes-de-timeout.
Melhor padrão:
- Aplicar breaker por tentativa ou contar cada tentativa falha na janela de falhas.
- Limitar orçamento total de retentativas com contexto de deadline compartilhado entre tentativas.
- Desabilitar retentativas quando circuito está open — fail fast no limite do breaker.
- Usar operações idempotentes só em rotas de retentativa; POST não idempotente sem chaves de idempotência não deve retentar às cegas.
async function callPaymentProvider(
ctx: RequestContext,
payload: ChargeRequest,
): Promise<ChargeResult> {
if (paymentBreaker.isOpen()) {
return fallbackQueue.enqueue(payload)
}
const deadline = Date.now() + 2_000
let lastError: Error | undefined
for (let attempt = 0; attempt < 2; attempt++) {
if (Date.now() > deadline) break
try {
const result = await paymentClient.charge(payload, {
signal: AbortSignal.timeout(800),
})
paymentBreaker.recordSuccess()
return result
} catch (err) {
lastError = err as Error
paymentBreaker.recordFailure()
if (paymentBreaker.isOpen()) break
}
}
return fallbackQueue.enqueue(payload)
}Quando o circuito abre durante checkout, o incidente não é só "payment API down" — é "pedidos entrando em fila de reconciliação." Esse comportamento deve aparecer em incident response runbooks com passos de verificação: profundidade da fila, idade da mensagem mais antiga, checagens de cobrança duplicada quando o provedor se recupera.
O que deve entrar em uma configuração de circuit breaker em produção?
Quando um circuito deve abrir por latência em vez de erros?
Abrir por latência quando a dependência retorna HTTP 200 com lentidão inutilizável — modo de falha comum em bancos sobrecarregados e APIs rate-limited que respondem devagar em vez de rejeitar. Tratar respostas mais lentas que o deadline do caller como falhas para o breaker mesmo que o status code seja sucesso. Combinar com timeouts upstream para que sucessos lentos nunca segurem recursos além do que o SLO visível ao usuário permite.
Como probing half-open evita flapping de recuperação?
Flapping ocorre quando probe bem-sucedido fecha o circuito, tráfego volta a inundar e a dependência falha de novo em segundos. Mitigações: exigir múltiplos sucessos consecutivos em half-open antes de fechar; rampar concorrência permitida gradualmente; usar duração open mais longa após reabertura que após primeira abertura. Registrar cada falha half-open — falhas repetidas costumam indicar que a dependência se recupera em rajadas, não curada por completo.
Fallbacks devem escrever no banco ou só retornar dados em cache?
Rotas de leitura: cache stale com header stale-while-revalidate ou campo de resposta costuma ser seguro quando o produto tolera minutos de lag. Rotas de escrita: preferir enqueue durável a drop silencioso. Fallback que descarta payment intent é pior que pagamento lento. Documentar se a rota fallback é lossy, at-least-once ou exactly-once e testar recuperação em consequência.
A common argument runs the other way
Um argumento frequente sustenta que circuit breakers escondem problemas de dependências, incentivam fallbacks preguiçosos e adicionam complexidade operacional melhor gasta consertando causas raiz — que timeouts e bons SLOs em vendors upstream são suficientes.
Timeouts sozinhos não param tempestades de retentativas de centenas de instâncias. SLOs de vendor não impedem que um blip regional de DNS sature pools de conexão antes de alguém ler a status page. Circuit breakers não substituem consertar dependências; limitam quanto tempo a aplicação participa de falha em cascata enquanto a reparação acontece. Equipes que nunca abrem circuito ou têm limiares frouxos demais ou não instrumentam transições de estado — ambos são gaps de configuração, não evidência de que o padrão falhou.
Key takeaways
- Isolamento de falhas com circuit breaker converte doença de dependência de starvation de threads em fast-fail com fallback definido.
- Breakers por dependência, janelas deslizantes e contagens mínimas de chamadas evitam abrir por ruído.
- Retentativas devem contar para taxas de falha do breaker; nunca retentar contra circuito open.
- Probes half-open precisam de regras de sucesso consecutivo para evitar flapping de recuperação.
- Instrumentar eventos open, half-open e close junto a SLIs — fallbacks mascarados ainda consomem error budget.
- Runbooks devem cobrir rotas fallback: profundidade de fila, reconciliação e checagens de operações duplicadas.
Conclusion
Dependências falham em câmera lenta com mais frequência do que com connection refused limpo. A frota paga essa lentidão quando cada serviço retenta indefinidamente contra o mesmo endpoint saturado. Circuit breakers são a camada de admission control para confiança de saída: limiares explícitos, fallbacks explícitos, probes de recuperação explícitos.
A próxima integração de dependência deve sair com configuração de breaker revisada no mesmo PR que o client wrapper — não adicionada após o primeiro outage encher o postmortem com "retry storm." Isolamento de falhas é decisão de design. É mais barato antes do tráfego de produção provar que o default é nenhum isolamento.