El patrón Circuit Breaker: fallar con elegancia
Cuando un servicio del que dependes está fallando, seguir enviando peticiones empeora todo — el patrón circuit breaker detiene la cascada fallando rápido.

Tu servicio de pagos está caído. Cada petición a tu API dispara una llamada al servicio de pagos, que agota el tiempo de espera tras 30 segundos. Tu pool de hilos se llena. Tu API deja de responder. Los usuarios ni siquiera pueden navegar por los productos: una caída de pagos ha escalado hasta convertirse en un fallo total del sistema. El patrón circuit breaker evita esto monitorizando las tasas de fallo y cortocircuitando las llamadas a servicios que fallan, devolviendo un error de inmediato en lugar de esperar a un timeout.
Los tres estados
Un circuit breaker tiene tres estados: cerrado (funcionamiento normal, las peticiones pasan), abierto (el servicio está fallando, las peticiones se rechazan inmediatamente) y semiabierto (se comprueba si el servicio se ha recuperado).
type CircuitState = "closed" | "open" | "half-open";
class CircuitBreaker<T> {
private state: CircuitState = "closed";
private failureCount = 0;
private successCount = 0;
private lastFailureTime = 0;
constructor(
private readonly fn: () => Promise<T>,
private readonly options: {
failureThreshold: number; // Failures before opening
resetTimeout: number; // Ms before trying half-open
halfOpenRequests: number; // Successful requests to close
}
) {}
async execute(): Promise<T> {
if (this.state === "open") {
if (Date.now() - this.lastFailureTime > this.options.resetTimeout) {
this.state = "half-open";
this.successCount = 0;
} else {
throw new CircuitOpenError("Circuit breaker is open");
}
}
try {
const result = await this.fn();
if (this.state === "half-open") {
this.successCount++;
if (this.successCount >= this.options.halfOpenRequests) {
this.state = "closed";
this.failureCount = 0;
}
} else {
this.failureCount = 0;
}
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = "open";
}
throw error;
}
}
getState(): CircuitState {
return this.state;
}
}
class CircuitOpenError extends Error {
constructor(message: string) {
super(message);
this.name = "CircuitOpenError";
}
}Uso del circuit breaker
Envuelve cualquier llamada externa que pueda fallar con un circuit breaker. Cuando el circuito se abre, quien llama recibe un error inmediato en lugar de esperar a un timeout.
// ❌ Direct call — 30s timeout when payment service is down
async function createOrder(order: OrderData): Promise<OrderResult> {
const payment = await fetch("http://payment-service/charge", {
method: "POST",
body: JSON.stringify({ amount: order.total }),
signal: AbortSignal.timeout(30000), // 30 seconds of waiting
});
return payment.json();
}
// ✅ Circuit breaker — fails immediately when service is known to be down
const paymentBreaker = new CircuitBreaker(
() => fetch("http://payment-service/charge", {
method: "POST",
body: JSON.stringify({ amount: order.total }),
signal: AbortSignal.timeout(5000),
}).then(r => r.json()),
{
failureThreshold: 5, // Open after 5 consecutive failures
resetTimeout: 30000, // Try again after 30 seconds
halfOpenRequests: 3, // Need 3 successes to fully close
}
);
async function createOrder(order: OrderData): Promise<OrderResult> {
try {
return await paymentBreaker.execute();
} catch (error) {
if (error instanceof CircuitOpenError) {
// Fail fast — return a meaningful response
return {
status: "pending",
message: "Payment processing is temporarily unavailable. Your order has been saved and will be processed shortly.",
};
}
throw error;
}
}Estrategias de fallback
Cuando el circuito se abre, tienes opciones más allá de simplemente devolver un error.
// Strategy 1: Cached response
async function getProductPrice(productId: string): Promise<number> {
try {
return await pricingBreaker.execute();
} catch (error) {
if (error instanceof CircuitOpenError) {
// Return cached price
const cached = await cache.get(`price:${productId}`);
if (cached) return Number(cached);
}
throw error;
}
}
// Strategy 2: Queue for retry
async function processPayment(order: OrderData): Promise<PaymentResult> {
try {
return await paymentBreaker.execute();
} catch (error) {
if (error instanceof CircuitOpenError) {
// Queue for later processing
await paymentQueue.enqueue({
orderId: order.id,
amount: order.total,
retryAfter: Date.now() + 60000,
});
return { status: "queued", message: "Payment will be processed when service recovers" };
}
throw error;
}
}
// Strategy 3: Degraded response
async function getRecommendations(userId: string): Promise<Product[]> {
try {
return await recommendationBreaker.execute();
} catch (error) {
if (error instanceof CircuitOpenError) {
// Return popular products instead of personalized recommendations
return await getPopularProducts();
}
throw error;
}
}Monitorización del estado del circuit breaker
Expón métricas del circuit breaker para dashboards y alertas.
import { Counter, Gauge } from "prom-client";
const circuitStateGauge = new Gauge({
name: "circuit_breaker_state",
help: "Current state of circuit breaker (0=closed, 1=open, 2=half-open)",
labelNames: ["service"],
});
const circuitTripsCounter = new Counter({
name: "circuit_breaker_trips_total",
help: "Number of times the circuit breaker has opened",
labelNames: ["service"],
});
// Enhanced circuit breaker with metrics
class MonitoredCircuitBreaker<T> extends CircuitBreaker<T> {
constructor(
fn: () => Promise<T>,
options: CircuitBreakerOptions,
private readonly serviceName: string
) {
super(fn, options);
}
async execute(): Promise<T> {
const stateMap = { closed: 0, open: 1, "half-open": 2 };
circuitStateGauge.set({ service: this.serviceName }, stateMap[this.getState()]);
try {
return await super.execute();
} catch (error) {
if (this.getState() === "open") {
circuitTripsCounter.inc({ service: this.serviceName });
}
throw error;
}
}
}Configuración de los umbrales
Los umbrales del circuit breaker dependen del comportamiento esperado del servicio y de tu tolerancia a los fallos.
// Low-latency, high-reliability service (payment processing)
const paymentBreaker = new CircuitBreaker(paymentCall, {
failureThreshold: 3, // Open quickly — payments are critical
resetTimeout: 15000, // Retry soon
halfOpenRequests: 5, // Require more successes to restore trust
});
// High-latency, best-effort service (recommendations)
const recommendationBreaker = new CircuitBreaker(recommendationCall, {
failureThreshold: 10, // More tolerant — recommendations are optional
resetTimeout: 60000, // Check less frequently
halfOpenRequests: 2, // Fewer successes needed
});Ideas clave
- Los circuit breakers evitan fallos en cascada — fallar rápido es mejor que bloquear recursos esperando timeouts
- Tres estados gobiernan el comportamiento — cerrado deja pasar, abierto rechaza de inmediato, semiabierto comprueba la recuperación
- Diseña fallbacks con sentido — datos en caché, reintentos encolados o respuestas degradadas son mejores que errores
- Monitoriza el estado del circuito — un circuito abierto es una señal operativa que requiere atención
- Ajusta los umbrales por servicio — los servicios críticos deberían abrirse rápido, los opcionales pueden ser más tolerantes
- Combínalo con timeouts y reintentos — los circuit breakers complementan (no reemplazan) la configuración de timeouts a nivel de petición


