Circuit Breaker en Agentes IA: Para Antes de Arder
Tu agente llama al LLM. Falla. Reintenta. Falla de nuevo. Cuatro reintentos con backoff exponencial: 1s, 2s, 4s, 8s. Quince segundos de espera. Y mientras tanto, los cuarenta usuarios activos en ese momento están haciendo exactamente lo mismo.
El LLM lleva dos minutos caído. Ningún reintento va a cambiar eso. Pero tu sistema sigue intentándolo, cuarenta veces por segundo, amplificando la presión sobre un servicio que no responde.
El retry te protege de los fallos transitorios. El circuit breaker te protege de los fallos sostenidos. Son dos patrones diferentes y ambos son necesarios en producción.
El cascade de fallos que no aparece en los logs
El fallo real del LLM dura 90 segundos. El impacto en tu sistema dura 10 minutos.
Sin circuit breaker, cada nuevo request activa su propia cadena de reintentos. Los timeouts se acumulan. El pool de conexiones se satura. Las latencias se disparan en toda la aplicación, no solo en las llamadas al LLM. Una caída de proveedor de 90 segundos se convierte en un incidente de 10 minutos porque cada request sigue intentando hasta agotar sus cuatro reintentos.
El circuit breaker corta ese ciclo. Cuando detecta que el servicio está caído, deja de intentarlo. Las peticiones que llegan durante la caída fallan rápido —en milisegundos, no en 15 segundos— y el sistema puede devolver una respuesta alternativa sin saturarse.
Esto es precisamente lo que el patrón de cadenas de fallback en agentes IA asume que ya tienes cuando menciona circuitBreaker.isHealthy(). Este artículo construye ese componente.
Tres estados, una sola regla
El circuit breaker tiene tres estados con transiciones predecibles:
CLOSED (circuito cerrado, operación normal): Las peticiones pasan al LLM con normalidad. El circuit breaker cuenta los fallos dentro de una ventana de tiempo. Si los fallos superan el umbral configurado, transiciona a OPEN.
OPEN (circuito abierto, cortado): Las peticiones fallan de inmediato sin llegar al LLM. El sistema responde en milisegundos con un fallback. Después de un tiempo configurable (típicamente 30 segundos), transiciona a HALF_OPEN para comprobar si el servicio se recuperó.
HALF_OPEN (probando recuperación): Deja pasar una petición de prueba. Si tiene éxito, el circuito cierra y vuelve a la operación normal. Si falla, vuelve a abrir.
La regla central: en estado OPEN, el sistema falla rápido y sirve una alternativa. Integrado con un servicio de agentes IA bien diseñado, el usuario no percibe la caída del proveedor.
Implementación completa en TypeScript
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
interface CircuitBreakerConfig {
failureThreshold: number; // Fallos para abrir
monitoringWindow: number; // Ventana en ms para contar fallos
resetTimeout: number; // ms en OPEN antes de probar
halfOpenSuccessThreshold: number; // Éxitos para cerrar desde HALF_OPEN
}
class CircuitBreaker {
private state: CircuitState = 'CLOSED';
private failureTimestamps: number[] = [];
private halfOpenSuccesses = 0;
private openedAt: number | null = null;
constructor(
readonly name: string,
private readonly config: CircuitBreakerConfig
) {}
isHealthy(): boolean {
this.maybeTransitionToHalfOpen();
return this.state !== 'OPEN';
}
recordFailure(): void {
const now = Date.now();
this.failureTimestamps = this.failureTimestamps.filter(
ts => now - ts < this.config.monitoringWindow
);
this.failureTimestamps.push(now);
if (this.state === 'HALF_OPEN') {
this.open();
} else if (this.failureTimestamps.length >= this.config.failureThreshold) {
this.open();
}
}
recordSuccess(): void {
if (this.state === 'HALF_OPEN') {
this.halfOpenSuccesses++;
if (this.halfOpenSuccesses >= this.config.halfOpenSuccessThreshold) {
this.close();
}
}
}
private maybeTransitionToHalfOpen(): void {
if (
this.state === 'OPEN' &&
this.openedAt !== null &&
Date.now() - this.openedAt >= this.config.resetTimeout
) {
this.state = 'HALF_OPEN';
this.halfOpenSuccesses = 0;
}
}
private open(): void {
this.state = 'OPEN';
this.openedAt = Date.now();
console.warn(`[CircuitBreaker:${this.name}] OPEN — ${this.failureTimestamps.length} fallos en ventana`);
}
private close(): void {
const recoveryTime = this.openedAt ? Date.now() - this.openedAt : 0;
this.state = 'CLOSED';
this.failureTimestamps = [];
this.halfOpenSuccesses = 0;
this.openedAt = null;
console.info(`[CircuitBreaker:${this.name}] CLOSED — recuperado en ${recoveryTime}ms`);
}
}
Parámetros de partida recomendados:
failureThreshold: 5— Cinco fallos dentro de la ventana abren el circuitomonitoringWindow: 60_000— Ventana deslizante de 60 segundosresetTimeout: 30_000— 30 segundos en OPEN antes de probar recuperaciónhalfOpenSuccessThreshold: 2— Dos éxitos consecutivos para cerrar definitivamente
Integración con el cliente de Anthropic en Next.js
// lib/circuit-breakers.ts — singleton compartido entre requests
export const anthropicBreaker = new CircuitBreaker('anthropic', {
failureThreshold: 5,
monitoringWindow: 60_000,
resetTimeout: 30_000,
halfOpenSuccessThreshold: 2,
});
// app/api/agent/route.ts
import Anthropic from '@anthropic-ai/sdk';
import { anthropicBreaker } from '@/lib/circuit-breakers';
const client = new Anthropic();
export async function callAgent(prompt: string): Promise<string> {
if (!anthropicBreaker.isHealthy()) {
return getFallbackResponse(prompt);
}
try {
const response = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }],
});
anthropicBreaker.recordSuccess();
const content = response.content[0];
return content.type === 'text' ? content.text : '';
} catch (error) {
anthropicBreaker.recordFailure();
throw error;
}
}
El detalle que lo arruina todo
El circuit breaker debe ser un singleton de módulo. Si lo instancias por request en Next.js, cada petición tiene su propio conteo de fallos y el circuito nunca se abre.
// ❌ INCORRECTO — cada request instancia su propio circuit breaker
export async function POST(request: Request) {
const breaker = new CircuitBreaker('anthropic', config); // nunca llega a abrir
// ...
}
// ✅ CORRECTO — importas el singleton compartido
import { anthropicBreaker } from '@/lib/circuit-breakers';
En Vercel Edge con múltiples instancias simultáneas, el estado del circuito necesita almacenamiento externo (Redis o Vercel KV) para ser compartido entre instancias. En un servidor de proceso único o en un contenedor Docker, el singleton de módulo es suficiente.
Qué monitorizar
Dos métricas bastan para empezar: duración de apertura y tasa de respuestas en fallback.
Las transiciones de estado ya están logueadas en la implementación anterior. Para un equipo pequeño, una alerta cuando el circuito lleva más de 60 segundos abierto cubre el 90% de los casos relevantes. El timestamp de openedAt te da la duración del incidente del proveedor directamente, sin instrumentación adicional.
A medida que escales, añade la métrica de porcentaje de requests que se están sirviendo desde el fallback. Si supera el 5% sostenido, hay algo más que un fallo de proveedor.
Conclusión
El circuit breaker no previene los fallos del LLM. Los aísla. Tu sistema deja de amplificarlos, deja de saturarse bajo carga y puede servir una alternativa mientras espera que el proveedor se recupere.
La implementación completa son menos de 60 líneas de TypeScript. El coste de no tenerla se mide en incidentes que duran diez veces más de lo necesario.
¿Tu agente en producción tiene circuit breaker? Si no estás seguro, probablemente no. Revisamos la arquitectura de tu sistema juntos →