Imagine: você tem uma campanha de Black Friday rodando e dispara 10.000 mensagens em 2 minutos para uma única instância WhatsApp. O que acontece? Spoiler: nada bom. Sua instância recebe rate limiting do WhatsApp, começa a falhar 30% das requisições, sua aplicação fica ocupada fazendo retry de mensagens que vão falhar de novo, o gateway sobrecarrega, e em alguns minutos você está com a instância em modo de "supervisão" — uma forma educada de dizer "quase banida".
O padrão circuit breaker existe exatamente para esse cenário. Ele detecta que o sistema downstream está degradado e para de mandar requisições até ele se recuperar. Em vez de empurrar trabalho para um sistema doente, você dá tempo dele respirar.
O que acontece sem proteção
Cenário real de um e-commerce que mandou disparo em massa em 2024:
- Minuto 0-2: 10k mensagens enfileiradas, taxa de envio 100/seg, tudo ok
- Minuto 2-5: WhatsApp começa a retornar 429 (rate limit) em ~30% das mensagens. App faz retry imediato. Volume real triplica para 300/seg.
- Minuto 5-8: Gateway saturado, latência de 2s vai para 30s, timeouts em massa. Aplicação faz mais retry achando que foi erro de rede.
- Minuto 8-15: Instância marcada como suspeita pelo WhatsApp. Mensagens param de chegar nos clientes (sumiram em algum lugar do meio do caminho). Usuário liga reclamando.
- Resultado: 4.000 mensagens entregues, 6.000 perdidas, 3 horas para o WhatsApp normalizar a sessão. Campanha foi para o lixo.
Com circuit breaker no lugar, no minuto 3 o sistema teria parado de mandar por 30 segundos, deixado a fila acumular, e voltado quando o gateway estivesse saudável. Resultado seria: campanha 30 min mais lenta, mas 9.700 das 10.000 mensagens entregues.
Os 3 estados
CLOSED (operação normal)
Tudo passa. Cada falha é contada. Se a contagem ultrapassar o threshold (ex.: 5 falhas em 1 minuto), muda para OPEN.
OPEN (curto-circuito ativo)
Toda requisição é imediatamente rejeitada, sem chegar no gateway. O cliente da função recebe um erro CircuitBreakerOpenError e decide o que fazer (enfileirar, alertar, descartar). Após um timeout (ex.: 30s), muda para HALF_OPEN.
HALF_OPEN (teste cuidadoso)
Permite passar até N requisições (ex.: 3) para testar se o gateway voltou. Se as N passarem, volta para CLOSED. Se qualquer falhar, volta para OPEN com timeout dobrado (backoff exponencial).
Implementação completa em Node.js
// circuit-breaker.ts
type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
interface CircuitOptions {
failureThreshold: number; // ex.: 5 falhas
windowMs: number; // janela de contagem (ex.: 60_000)
openTimeoutMs: number; // tempo no OPEN (ex.: 30_000)
halfOpenMaxAttempts: number; // tentativas em HALF_OPEN (ex.: 3)
name: string; // identificação para logs/métricas
}
export class CircuitBreaker {
private state: CircuitState = "CLOSED";
private failures: number[] = []; // timestamps de falhas recentes
private openedAt = 0;
private halfOpenAttempts = 0;
private halfOpenSuccesses = 0;
private currentTimeoutMs: number;
constructor(private opts: CircuitOptions) {
this.currentTimeoutMs = opts.openTimeoutMs;
}
async execute(fn: () => Promise): Promise {
this.transitionIfNeeded();
if (this.state === "OPEN") {
throw new Error(`CircuitBreaker[${this.opts.name}] is OPEN`);
}
if (this.state === "HALF_OPEN") {
if (this.halfOpenAttempts >= this.opts.halfOpenMaxAttempts) {
throw new Error(`CircuitBreaker[${this.opts.name}] HALF_OPEN limit reached`);
}
this.halfOpenAttempts++;
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private transitionIfNeeded() {
const now = Date.now();
if (this.state === "OPEN" && now - this.openedAt >= this.currentTimeoutMs) {
this.state = "HALF_OPEN";
this.halfOpenAttempts = 0;
this.halfOpenSuccesses = 0;
}
// Limpa falhas fora da janela
this.failures = this.failures.filter(t => now - t < this.opts.windowMs);
}
private onSuccess() {
if (this.state === "HALF_OPEN") {
this.halfOpenSuccesses++;
if (this.halfOpenSuccesses >= this.opts.halfOpenMaxAttempts) {
this.state = "CLOSED";
this.failures = [];
this.currentTimeoutMs = this.opts.openTimeoutMs; // reset backoff
}
} else if (this.state === "CLOSED") {
// ok, nada a fazer
}
}
private onFailure() {
if (this.state === "HALF_OPEN") {
this.state = "OPEN";
this.openedAt = Date.now();
this.currentTimeoutMs = Math.min(this.currentTimeoutMs * 2, 5 * 60_000); // dobra até 5min
return;
}
this.failures.push(Date.now());
if (this.failures.length >= this.opts.failureThreshold) {
this.state = "OPEN";
this.openedAt = Date.now();
}
}
getState() { return this.state; }
}
Integrando com BullMQ
Sua fila de mensagens deve respeitar o breaker. Se ele estiver aberto, o job volta para a fila (não é descartado).
import { Worker } from "bullmq";
import axios from "axios";
import { CircuitBreaker } from "./circuit-breaker";
const ZAP = axios.create({
baseURL: "https://api.zap-api.tech/v1",
headers: { Authorization: `Bearer ${process.env.ZAP_TOKEN}` },
});
// Um breaker por instância — uma instância em apuros não derruba as outras
const breakers = new Map();
function breakerFor(instanceId: string) {
if (!breakers.has(instanceId)) {
breakers.set(instanceId, new CircuitBreaker({
name: instanceId,
failureThreshold: 5,
windowMs: 60_000,
openTimeoutMs: 30_000,
halfOpenMaxAttempts: 3,
}));
}
return breakers.get(instanceId)!;
}
new Worker("messages", async (job) => {
const { instanceId, phone, text } = job.data;
const breaker = breakerFor(instanceId);
await breaker.execute(async () => {
await ZAP.post(`/instances/${instanceId}/messages`, {
to: phone, type: "text", text,
});
});
}, {
// Configurar BullMQ para fazer retry com backoff
// Quando o breaker abrir, jobs falham e BullMQ reagenda
});
Webhook de throttle
A ZAP API expõe webhook quando detecta que sua instância está perto de rate-limit. Use isso como sinal externo para abrir o breaker preventivamente.
app.post("/webhook/zap", async (req, res) => {
res.status(200).send("ok");
if (req.body.type === "instance.throttle") {
const breaker = breakerFor(req.body.instanceId);
// força abertura por 60s
await breaker.execute(async () => { throw new Error("preemptive"); }).catch(() => {});
// (na implementação real, expor método breaker.forceOpen())
}
});
Métricas que importam
Sem métricas, o breaker é uma caixa preta. Exponha:
- Estado atual por instância (CLOSED/OPEN/HALF_OPEN)
- Total de aberturas nas últimas 24h
- Taxa de erro upstream (% requisições que falham antes do breaker)
- Latência p95/p99 das requisições que passam
- Tempo médio em OPEN — se for muito alto, gateway downstream tem problema sério
Em Prometheus/Grafana, alerte quando o estado fica OPEN por mais de 5 minutos seguidos: significa que o problema downstream não é transitório, e você precisa intervir manualmente.
Casos práticos
Caso 1: SaaS de cobrança
Plataforma de cobrança PIX que dispara lembretes para 50.000 inadimplentes/dia. Antes do breaker, dia primeiro do mês era sempre caos: gateway congestionado, 20% das mensagens perdidas. Com breaker por instância (5 instâncias rodando), a campanha leva 4h em vez de 1h, mas entrega 99,1% das mensagens.
Caso 2: E-commerce na Black Friday
Loja virtual com pico de 8.000 confirmações de pedido em 30 minutos. Breaker abre 12 vezes ao longo da campanha, cada vez por ~20 segundos. Cliente nem percebe — BullMQ reagenda. Sem o breaker no ano anterior, foi rate-limit total e instância suspensa por 4h, prejuízo estimado de R$80k em pedidos não confirmados.
Caso 3: Bot de atendimento de saúde
Bot de uma operadora de saúde recebe pico de 200 mensagens/min após mandar comunicado em massa. Breaker protegeu o sistema de OCR/IA por trás (cada mensagem chama OpenAI) — quando OpenAI ficou lenta, breaker abriu, sistema voltou a responder com mensagem de "estamos com volume alto, retornaremos em instantes" e enfileirou para depois.
FAQ
Qual a diferença entre circuit breaker e retry?
Retry tenta de novo a mesma requisição. Circuit breaker decide se vale a pena tentar. Eles trabalham juntos: o retry só faz sentido se o breaker estiver fechado. Tentar 5x quando você sabe que o sistema está fora é desperdício e piora o problema.
Devo ter um breaker global ou um por instância?
Por instância. Uma instância com problema não deve afetar as outras. Custos: pequena memória extra (alguns kB por breaker). Benefícios: isolamento real e diagnóstico granular ("qual instância está doente?").
Falso positivo é problema?
Sim, e a defesa principal é o threshold. Se você abrir o breaker com 1 falha, qualquer hiccup de rede para tudo. Use threshold de 5+ falhas em janela de 60s e timeout inicial de 30s. Cresça o threshold se ainda assim sentir abertura desnecessária.
Como observar o breaker em produção?
Exporte métricas Prometheus: circuit_state{name="..."} (0=closed, 1=halfopen, 2=open), circuit_failures_total, circuit_opens_total. Crie alerta no Grafana: estado=OPEN por >5min envia notificação no Slack.
Como recupero da abertura automaticamente?
O HALF_OPEN cuida disso. Após o timeout, deixa passar N requisições de teste — se passarem, fecha. Se falharem, dobra o timeout (exponencial) até no máximo 5min. Depois de problema persistente por horas, alguém precisa investigar manualmente — não tente recuperar de tudo automaticamente.
Próximo passo
Implemente o breaker acima em uma tarde e proteja sua operação do próximo pico inesperado. Criar conta grátis e comece a integrar.