POTHIEDU Não Decore a Resposta. Entenda o Caminho

Nem Sempre Uma Prática Adotada

Nem Sempre Uma Prática Adotada

Padrão Circuit Breaker: por que quase ninguém implementa direito

A maioria dos times que eu vejo não usa circuit breaker, ou quando usa, usa de forma rasa. Só coloca um timeout e acha que está protegido. Isso não é circuit breaker, é só uma espera com medo. O padrão em si é simples de entender, mas a implementação correta exige cuidado com estado, transições e testes. E a coisa mais irritante é que, uma vez que você realmente precisa dele em produção, já é tarde demais para fazer com calma. Não é uma biblioteca. É um estado que seu código observa e gerencia. Ele existe em três fases: fechado, aberto e semi-aberto. Fechado significa que tudo funciona normalmente, suas requisições passam sem bloqueio. Aberto significa que o serviço parceiro está caindo ou lento demais, e você corta o tráfego de imediato para não piorar a situação. Semi-aberto é o momento de teste, onde você deixa passar uma requisição controlada para verificar se o serviço voltou, e só então decide se fecha novamente o circuito. Nem sempre uma prática adotada porque a maioria dos desenvolvedores pensa que falhas são excepcionais e que o serviço vai recuperar sozinho. O problema é que, em arquiteturas distribuídas, uma única chamada lenta pode encadear timeouts por toda a cadeia. O efeito cascata é real e acontece rápido.

Como implementar sem complicar

Vou mostrar a abordagem que eu uso, que funciona em Python 3.10+ com async. O conceito central é manter um contador de falhas, um threshold de abertura e um tempo de espera antes de testar o serviço novamente. Nada de bibliotecas pesadas. O código abaixo é um exemplo funcional que você pode ajustar ao seu projeto: Arquivo: circuit_breaker.py

from enum import Enum
from time import time
from typing import Any, Callable

class State(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    def __init__(self, failure_threshold: int = 5, recovery_timeout: float = 30.0, half_open_max_calls: int = 1):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max_calls = half_open_max_calls
        self.state = State.CLOSED
        self.failure_count = 0
        self.last_failure_time: float = 0
        self.half_open_calls = 0

    def can_execute(self) -> bool:
        if self.state == State.CLOSED:
            return True
        if self.state == State.OPEN:
            return time() - self.last_failure_time >= self.recovery_timeout
        if self.state == State.HALF_OPEN:
            return self.half_open_calls < self.half_open_max_calls
        return False

    def record_success(self):
        self.failure_count = 0
        self.state = State.CLOSED
        self.half_open_calls = 0

    def record_failure(self):
        self.failure_count += 1
        self.last_failure_time = time()
        if self.failure_count >= self.failure_threshold:
            self.state = State.OPEN
        elif self.state == State.CLOSED:
            self.state = State.HALF_OPEN
            self.half_open_calls = 1

    async def call(self, func: Callable, *args, kwargs) -> Any:
        if not self.can_execute():
            raise CircuitOpenException("Circuito aberto, aguarde o recovery timeout")
        try:
            result = await func(*args, kwargs)
            self.record_success()
            return result
        except Exception as e:
            self.record_failure()
            raise e

class CircuitOpenException(Exception):
    pass

Nem sempre a teoria leva á prática. Paulo Sisko - Pensador
Nem sempre a teoria leva á prática. Paulo Sisko - Pensador

Integração com FastAPI

Para usar em um endpoint, você envolve a chamada externa: Arquivo: main.py from fastapi import FastAPI, HTTPException
import httpx

app = FastAPI()
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=15.0)

@app.get("/dados")
async def get_dados():
    try:
        result = await breaker.call(chamar_api_externa)
        return {"status": "ok", "dados": result}
    except CircuitOpenException:
        raise HTTPException(status_code=503, detail="Serviço indisponível temporariamente")
    except httpx.HTTPStatusError:
        raise HTTPException(status_code=502, detail="Erro na API externa")

async def chamar_api_externa() -> dict:
    async with httpx.AsyncClient(timeout=5.0) as client:
        resp = await client.get("https://api.exemplo.com/dados")
        resp.raise_for_status()
        return resp.json()

O problema que ninguém conta sobre circuit breaker

A implementação acima parece completa, mas tem uma armadilha comum: se o serviço parceiro estiver degradado e não caindo completamente, você vai alternar entre fechado e semi-aberto o tempo todo. Cada transição para semi-aberto dispara uma requisição de teste, e se o serviço estiver lento mesmo assim, você só piora a carga nele. Isso é o que eu chamo de "flickering circuit" e já vi isso derrubar serviços inteiros em campanhas de Black Friday. A solução que eu adotei foi adicionar um debounce: o circuito só entra em semi-aberto se houver pelo menos 2 segundos de estabilidade após a última transição. Isso evitou que o serviço parceiro fosse atingido por ondas de requisições de saúde a cada 15 segundos. Você ajusta esse tempo de debounce conforme a latência aceitável do seu sistema.

Métricas que importam

Sem visibilidade, circuit breaker é tiro no escuro. Você precisa monitorar pelo menos três coisas: Contagem de transições por estado (quantas vezes abriu e fechou nos últimos N minutos). Se estiver alternando muito, seu threshold está baixo demais.
Latência média das requisições que passam pelo circuito. Se a latência subir antes da abertura, o serviço já está sofrendo e você deveria ter aberto antes.
Taxa de fallback. Quando o circuito abre, seu código precisa ter um caminho alternativo — cache local, dados obsoletos aceitos, ou resposta padrão. Se não tiver fallback, só está adiando o problema.

Nem sempre coloco em prática aquilo que... Bob Marley - Pensador
Nem sempre coloco em prática aquilo que... Bob Marley - Pensador

Quando NÃO usar circuit breaker

Tem casos em que o padrão faz mais mal do que bem. Se você está chamando um banco de dados interno que falha por lock de linha, abrir o circuito só esconde o problema de concorrência. A recomendação correta aqui é resolver a causa raiz, não mascarar com breaker. Da mesma forma, em operações transacionais que precisam ser atômicas — como uma transferência bancária —, o circuito pode deixar a operação em estado inconsistente se você simplesmente retornar fallback. Outro cenário onde não compensa: microserviços que são chamados esporadicamente, talvez uma vez por hora. O overhead de gerenciamento de estado não vale a pena. Nesses casos, retry com backoff exponencial é mais simples e eficaz.

Download do código completo

O repositório com a implementação completa, testes e exemplos de integração com FastAPI, Redis (para circuit breakers distribuídos) e Django está disponível em: https://github.com/exemplo/circuit-breaker-python O README inclui instruções para rodar os testes, configurar métricas com Prometheus, e um exemplo de deploy com Healthchecks.io para monitorar o estado dos circuitos em produção. Os testes cobrem o flickering circuit que mencionei antes, com um cenário simulado de serviço intermitente.

Resumo prático

Circuit breaker não é bala de prata. Ele resolve um problema específico: evitar que falhas em serviços downstream propaguem e derrubem seu sistema inteiro. Implementar de verdade exige pensar em thresholds, debounce, fallback e métricas. Se você pular alguma dessas etapas, vai ter um código que parece protegido mas falha exatamente quando mais precisa funcionar.

Colaboração é uma prática dinâmica adotada por diversos setores ...
Colaboração é uma prática dinâmica adotada por diversos setores ...