Circuit Breaker em Python Puro: O Disjuntor Que Impede Uma API Fora do Ar de Derrubar Todo o Seu Sistema em Cascata
Você já viu um sistema inteiro cair porque uma única API externa decidiu morrer no pior momento possível? Eu já. E não foi bonito.
Era uma terça-feira, 14h47. O sistema de cobrança do meu cliente parou de responder. Não tinha fallback. Não tinha proteção. Cada retry desesperado do nosso backend só piorava a situação — como tentar reanimar um paciente dando socos no peito com mais força. Em 4 minutos, 237 threads travadas, consumo de memória em 94%, e o load balancer começando a derrubar instâncias saudáveis por timeout.
Falência em cascata. O pesadelo de todo engenheiro de sistemas distribuídos.
Hoje eu vou te mostrar como implementar o padrão que impede exatamente isso: o Circuit Breaker em Python puro. Sem bibliotecas externas. Sem dependências exóticas. Apenas código que você pode colar no seu projeto agora e dormir tranquilo hoje à noite.
O Que É Um Circuit Breaker (E Por Que Você Precisa Dele Ontem)
O nome vem da engenharia elétrica. Sabe aquele disjuntor na sua caixa de luz? Quando a corrente passa do limite, ele desliga automaticamente. Não espera queimar. Não espera pegar fogo. Ele corta. Protege o resto do circuito.
No mundo de software, o Circuit Breaker faz exatamente a mesma coisa, só que com requisições HTTP:
- Quando um serviço externo começa a falhar, o circuit breaker detecta o padrão de erros.
- Após um threshold configurável, ele "abre o circuito" — para de enviar requisições para o serviço problemático.
- As requisições falham imediatamente, sem gastar timeout, sem consumir threads, sem drenar recursos.
- Após um tempo de cooldown, ele tenta uma requisição de teste (half-open). Se funcionar, fecha o circuito. Se falhar, abre de novo.
É simples. É elegante. E salva sistemas em produção todos os dias.
Os Três Estados do Circuit Breaker
Antes de codar, vamos entender a máquina de estados. Um circuit breaker tem exatamente três estados:
1. CLOSED (Fechado) — Operação Normal
O circuito está fechado, as requisições passam normalmente. O breaker está contando as falhas silenciosamente. Enquanto a taxa de erro estiver abaixo do threshold, nada acontece. É o estado de "confio que está tudo bem, mas estou de olho".
2. OPEN (Aberto) — Proteção Ativada
O threshold de falhas foi atingido. O circuito abre. Todas as requisições subsequentes falham imediatamente — sem chamar o serviço externo, sem gastar timeout, sem bloquear threads. É o equivalente digital a puxar o plugue da tomada.
3. HALF-OPEN (Semi-aberto) — Testando as Águas
Após um período de cooldown, o breaker permite uma única requisição de teste. Se ela succeed, o circuito fecha e a operação normal retorna. Se falhar, o circuito abre novamente e o timer de cooldown recomeça.

Assim como uma rede elétrica complexa precisa de proteção contra sobrecarga, sua arquitetura de microsserviços precisa de circuit breakers.
Implementação: Circuit Breaker do Zero em Python Puro
Vamos construir um circuit breaker completo, thread-safe, com contagem de falhas deslizante, estados bem definidos e métricas de observabilidade. Tudo em Python padrão — sem pip install de nada.
Primeiro, os estados:
from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Callable, Any, Optional
import time
import threading
class CircuitState(Enum):
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
Simples, explícito, sem mágica. Três estados, três valores. Se alguém olhar esse código daqui a 6 meses, vai entender em 2 segundos.
Agora, o Circuit Breaker propriamente dito:
@dataclass
class CircuitBreaker:
name: str
failure_threshold: int = 5
recovery_timeout: int = 30 # segundos
success_threshold: int = 2 # sucessos consecutivos para fechar
_failure_count: int = field(default=0, init=False)
_success_count: int = field(default=0, init=False)
_state: CircuitState = field(default=CircuitState.CLOSED, init=False)
_last_failure_time: Optional[datetime] = field(default=None, init=False)
_last_state_change: datetime = field(default_factory=datetime.now, init=False)
_lock: threading.Lock = field(default_factory=threading.Lock, init=False)
_total_calls: int = field(default=0, init=False)
_total_failures: int = field(default=0, init=False)
_total_successes: int = field(default=0, init=False)
_total_rejected: int = field(default=0, init=False)
_total_fallbacks: int = field(default=0, init=False)
@property
def state(self) -> CircuitState:
with self._lock:
if self._state == CircuitState.OPEN:
if self._should_attempt_reset():
self._transition_to(CircuitState.HALF_OPEN)
else:
return CircuitState.OPEN
return self._state
def _should_attempt_reset(self) -> bool:
if self._last_failure_time is None:
return False
elapsed = datetime.now() - self._last_failure_time
return elapsed.total_seconds() >= self.recovery_timeout
def _transition_to(self, new_state: CircuitState) -> None:
self._state = new_state
self._last_state_change = datetime.now()
if new_state == CircuitState.CLOSED:
self._failure_count = 0
self._success_count = 0
elif new_state == CircuitState.HALF_OPEN:
self._success_count = 0
print(f"[{self.name}] Transição para {new_state.value}")
Repara em alguns detalhes importantes: thread-safe, auto-transição OPEN→HALF_OPEN, métricas embutidas, e success threshold evitando falsos positivos.
O método principal — call():
def call(
self,
func: Callable,
fallback: Optional[Callable] = None,
*args,
**kwargs
) -> Any:
with self._lock:
current_state = self.state
self._total_calls += 1
if current_state == CircuitState.OPEN:
self._total_rejected += 1
if fallback:
self._total_fallbacks += 1
return fallback(*args, **kwargs)
raise CircuitOpenError(
f"Circuit '{self.name}' is OPEN. "
f"Rejeitando requisição para proteger o sistema."
)
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as exc:
self._on_failure()
if fallback:
self._total_fallbacks += 1
return fallback(*args, **kwargs)
raise
def _on_success(self) -> None:
with self._lock:
self._total_successes += 1
if self._state == CircuitState.HALF_OPEN:
self._success_count += 1
if self._success_count >= self.success_threshold:
self._transition_to(CircuitState.CLOSED)
elif self._state == CircuitState.CLOSED:
self._failure_count = 0
def _on_failure(self) -> None:
with self._lock:
self._total_failures += 1
self._last_failure_time = datetime.now()
if self._state == CircuitState.HALF_OPEN:
self._transition_to(CircuitState.OPEN)
elif self._state == CircuitState.CLOSED:
self._failure_count += 1
if self._failure_count >= self.failure_threshold:
self._transition_to(CircuitState.OPEN)
class CircuitOpenError(Exception):
pass
O lock protege apenas a verificação de estado e atualização de contadores. A execução real acontece fora do lock. Isso é crucial — segurar um lock durante uma chamada HTTP é um anti-pattern que transforma proteção em gargalo.
Exemplo Prático: Circuit Breaker em um Serviço de Pagamentos
Imagine que você tem um serviço de pagamentos externo que decide ficar instável:
import requests
from dataclasses import dataclass
@dataclass
class PaymentResult:
success: bool
transaction_id: str
message: str
payment_breaker = CircuitBreaker(
name="pagamento-externo",
failure_threshold=3,
recovery_timeout=60,
success_threshold=2
)
def cobrar_cliente(valor: float, cliente_id: str) -> PaymentResult:
response = requests.post(
"https://api.pagamento-externo.com/v1/cobrar",
json={"valor": valor, "cliente_id": cliente_id},
timeout=5
)
response.raise_for_status()
data = response.json()
return PaymentResult(
success=data["status"] == "ok",
transaction_id=data["tx_id"],
message=data.get("message", "")
)
def fallback_cobranca(valor: float, cliente_id: str) -> PaymentResult:
print(f"⚠️ Fallback: cobrança de R${valor:.2f} do cliente {cliente_id} registrada para retry assíncrono")
return PaymentResult(
success=False,
transaction_id="pending-retry",
message="Cobrança enfileirada para processamento assíncrono"
)
try:
resultado = payment_breaker.call(
cobrar_cliente,
fallback=fallback_cobranca,
valor=99.90,
cliente_id="cli_12345"
)
print(f"Resultado: {resultado.message}")
except CircuitOpenError as e:
print(f"⚡ Circuit breaker ativado: {e}")
except requests.RequestException as e:
print(f"❌ Erro na cobrança: {e}")
Percebe o que acontece?
- As primeiras 2 falhas passam — ainda é "normal".
- Na 3ª falha consecutiva, o circuito abre.
- Todas as requisições seguintes vão direto pro fallback.
- Após 60 segundos, uma requisição de teste é enviada.
- Se voltou, 2 sucessos consecutivos fecham o circuito.
- Se ainda falha, circuito reabre e espera mais 60 segundos.

Cada conexão num painel de controle representa uma dependência entre serviços. Uma falha mal gerenciada aqui = colapso em cascata.
Configurações que Importam (E Os Valores Que Eu Uso em Produção)
| Parâmetro | Valor Padrão | Por quê? |
|---|---|---|
failure_threshold |
3-5 | Pouco demais = falso positivo. Muito demais = dano antes de proteger. |
recovery_timeout |
30-60s | Tempo razoável para um serviço se recuperar de pico de carga. |
success_threshold |
2 | Um sucesso pode ser sorte. Dois já é tendência. |
Quando Ajustar Para Baixo
Se o serviço externo é crítico e raramente falha, use failure_threshold=3.
Quando Ajustar Para Cima
Se o serviço é naturalmente instável (ex: API de terceiros com SLA de 99%), use failure_threshold=5 ou mais.
🔥 O Perrengue Real
Implementei meu primeiro circuit breaker num sistema de notificações push. Coloquei
failure_threshold=2achando que era "conservador e seguro". Resultado: o serviço de push tinha um pico de latência de 200ms a cada 10 minutos. Meu circuit breaker abria a cada 10 minutos. Notificações iam todas pro fallback (email). Os usuários recebiam emails 8 minutos depois do push original. O CTO me chamou pra conversar. Desde então, meu threshold mínimo é 3 e eu monitore a taxa de falhas antes de decidir o número.
Circuit Breaker vs Retry: Quando Usar Cada Um
- Retry é para falhas transitórias — um timeout isolado, um 503 momentâneo.
- Circuit Breaker é para falhas persistentes — o serviço caiu, o banco travou, a rede caiu.
Eles são complementares. O padrão mais robusto é retry DENTRO do circuit breaker.
Observabilidade: Métricas Que Salvam Vidas
Um circuit breaker sem métricas é como um disjuntor sem etiqueta na caixa de luz.
def metrics(self) -> dict:
with self._lock:
return {
"name": self.name,
"state": self._state.value,
"failure_count": self._failure_count,
"total_calls": self._total_calls,
"total_failures": self._total_failures,
"total_successes": self._total_successes,
"total_rejected": self._total_rejected,
"total_fallbacks": self._total_fallbacks,
"failure_rate": (
self._total_failures / self._total_calls
if self._total_calls > 0 else 0
),
"last_state_change": self._last_state_change.isoformat(),
}
Configure alertas para:
- Circuit breaker aberto por mais de 5 minutos → alerta de alta prioridade
- Taxa de rejeição > 50% → serviço dependente está em colapso
- Fallback sendo acionado > 100x/hora → investigate a causa raiz
Quando NÃO Usar Circuit Breaker
- Operações síncronas críticas onde fallback não é aceitável.
- Serviços com baixa latência e alta confiabilidade (ex: localhost → banco local). O overhead pode não compensar.
- Quando o "serviço externo" é na verdade seu próprio código. Se sua função está falhando tanto, o problema está nela.
Resumo: O Checklist do Circuit Breaker
- ✅ Threshold de falhas baseado em dados reais
- ✅ Fallback implementado e testado
- ✅ Métricas expostas e monitoradas
- ✅ Alertas configurados para estado OPEN prolongado
- ✅ Recovery timeout ajustado ao SLA do serviço dependente
- ✅ Thread-safe
- ✅ Logs de transição de estado
- ✅ Testes unitários cobrindo as 3 transições
Se você implementou até aqui, parabéns. Você agora tem uma arma a mais contra falhas em cascata.
Qual automação ou padrão de engenharia você quer ver implementado do zero em Python puro? Me conta aqui embaixo. Os mais pedidos viram post.
📂 Veja mais artigos da categoria Lab da Garra →
Posts relacionados:
Dead Letter Queue em Python Puro — A quarentena que salva mensagens perdidas
Graceful Shutdown em Python Puro — O protocolo que faz seu processo morrer sem perder dados
Rate Limiter em Python Puro — O guardião que impede sua automação de virar um ataque DDoS
