O problema em cascata
Imagine que sua API depende de um serviço de pagamento externo que ficou lento. Cada chamada demora 30 segundos antes de dar timeout. Sua aplicação começa a acumular requests aguardando. Os recursos se esgotam. Sua API fica lenta. Depois para.
Uma falha num serviço externo derrubou a aplicação inteira.
O circuit breaker resolve isso: ele detecta que o serviço está com problema e para de chamar por um tempo, retornando erro imediatamente em vez de esperar o timeout. O serviço externo tem espaço para se recuperar. Sua aplicação continua funcionando.
Os três estados
O circuit breaker tem três estados:
- Closed: funcionamento normal, chamadas passam normalmente
- Open: circuito aberto, chamadas falham imediatamente sem chamar o serviço
- Half-open: testando recuperação, deixa passar algumas chamadas para ver se o serviço voltou
[Closed] ---(muitas falhas)---> [Open]
[Open] ---(timeout de recuperação)---> [Half-open]
[Half-open] ---(sucesso)---> [Closed]
[Half-open] ---(falha)---> [Open]
Usando a biblioteca opossum
Para Node.js, opossum é a biblioteca mais madura:
pnpm add opossum
pnpm add -D @types/opossumimport CircuitBreaker from "opossum";
async function callPaymentService(payload: PaymentPayload): Promise<PaymentResult> {
const response = await fetch("https://payments.example.com/charge", {
method: "POST",
body: JSON.stringify(payload),
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error(`Payment failed: ${response.status}`);
return response.json();
}
const breaker = new CircuitBreaker(callPaymentService, {
timeout: 5000, // timeout por chamada: 5s
errorThresholdPercentage: 50, // abre se 50% das chamadas falharem
resetTimeout: 30000, // tenta fechar após 30s no estado Open
volumeThreshold: 5, // mínimo de chamadas antes de avaliar threshold
});
// Fallback quando o circuito está aberto
breaker.fallback((payload: PaymentPayload) => {
throw new ServiceUnavailableException(
"Serviço de pagamento temporariamente indisponível. Tente em alguns instantes."
);
});
// Eventos para observabilidade
breaker.on("open", () => logger.warn("Circuit breaker aberto: payment service"));
breaker.on("close", () => logger.log("Circuit breaker fechado: payment service recuperado"));
breaker.on("halfOpen", () => logger.log("Circuit breaker half-open: testando payment service"));Integrando com NestJS
Crie um serviço wrapper para o circuit breaker:
@Injectable()
export class PaymentService {
private readonly breaker: CircuitBreaker;
private readonly logger = new Logger(PaymentService.name);
constructor(private readonly httpService: HttpService) {
this.breaker = new CircuitBreaker(
this.chargeInternal.bind(this),
{
timeout: 5000,
errorThresholdPercentage: 50,
resetTimeout: 30_000,
volumeThreshold: 5,
}
);
this.breaker.on("open", () =>
this.logger.warn("Circuit breaker OPEN: serviço de pagamento com falhas")
);
this.breaker.on("close", () =>
this.logger.log("Circuit breaker CLOSED: serviço de pagamento recuperado")
);
}
async charge(payload: ChargeDto): Promise<ChargeResult> {
try {
return await this.breaker.fire(payload);
} catch (error) {
if (error.name === "OpenCircuitError") {
throw new ServiceUnavailableException("Pagamentos temporariamente indisponíveis");
}
throw error;
}
}
private async chargeInternal(payload: ChargeDto): Promise<ChargeResult> {
const { data } = await firstValueFrom(
this.httpService.post<ChargeResult>(
`${process.env.PAYMENT_SERVICE_URL}/charge`,
payload,
{ timeout: 5000 }
)
);
return data;
}
}Circuit breaker com retry
Circuit breaker e retry trabalham juntos: retry trata falhas transitórias, circuit breaker trata serviços com problema persistente.
A ordem importa: retry dentro do circuit breaker, não o contrário.
const breaker = new CircuitBreaker(
async (payload: PaymentPayload) => {
// Retry interno: tenta 3 vezes antes de contar como falha para o breaker
for (let attempt = 1; attempt <= 3; attempt++) {
try {
return await callPaymentService(payload);
} catch (error) {
if (attempt === 3) throw error;
await new Promise((r) => setTimeout(r, 200 * attempt)); // 200ms, 400ms
}
}
},
{
timeout: 15000, // timeout maior para cobrir os retries
errorThresholdPercentage: 50,
resetTimeout: 60_000,
}
);Múltiplos circuit breakers por serviço
Para sistemas com múltiplas dependências externas, mantenha um breaker por serviço:
@Injectable()
export class CircuitBreakerRegistry {
private readonly breakers = new Map<string, CircuitBreaker>();
get(serviceName: string, fn: (...args: unknown[]) => Promise<unknown>): CircuitBreaker {
if (!this.breakers.has(serviceName)) {
const breaker = new CircuitBreaker(fn, {
timeout: 5000,
errorThresholdPercentage: 50,
resetTimeout: 30_000,
volumeThreshold: 5,
name: serviceName,
});
breaker.on("open", () =>
this.metrics.increment(`circuit_breaker.open`, { service: serviceName })
);
this.breakers.set(serviceName, breaker);
}
return this.breakers.get(serviceName)!;
}
getStats(): Record<string, object> {
const stats: Record<string, object> = {};
for (const [name, breaker] of this.breakers) {
stats[name] = breaker.stats;
}
return stats;
}
}Endpoint de health check
Exponha o estado dos circuit breakers para monitoramento:
@Get("health/circuit-breakers")
getCircuitBreakerHealth() {
const stats = this.circuitBreakerRegistry.getStats();
return {
status: Object.values(stats).every((s: any) => !s.opened) ? "healthy" : "degraded",
services: stats,
};
}{
"status": "degraded",
"services": {
"payment-service": {
"opened": true,
"failures": 45,
"successes": 12,
"latencyMean": 4823
},
"notification-service": {
"opened": false,
"failures": 2,
"successes": 198,
"latencyMean": 145
}
}
}Quando não usar circuit breaker
Circuit breakers adicionam complexidade. Nem toda chamada externa precisa:
- Banco de dados próprio: use pool de conexões com timeouts bem configurados
- Cache Redis: normalmente resiliente e de baixa latência
- Operações que não têm fallback: se você precisa obrigatoriamente de uma resposta, circuit breaker só muda quando o erro aparece, não o que fazer com ele
Use circuit breaker principalmente para serviços de terceiros onde você pode ter um fallback razoável ou onde a latência alta do timeout causaria dano maior que o erro imediato.
Conclusão
Circuit breaker é um dos patterns mais importantes para sistemas que dependem de serviços externos. Ele muda o comportamento de "falha em cascata silenciosa" para "falha rápida e localizada".
Com opossum no Node.js, a implementação é simples. O trabalho real está em definir os thresholds corretos para o seu padrão de tráfego e ter fallbacks que façam sentido para o usuário.