← SCRAM AI Lab
Los endpoints de LLMs fallan diferente que las APIs tradicionales. 429 y 529 son ritmo, no muerte. El patrón circuit breaker con HALF_OPEN evita inundar al provider en mal momento.
May 21, 2026
9 lecturas
Una API REST tradicional falla con 500, 502, 503 o timeout. Predecible. Los endpoints LLM fallan con: 429 (rate limit), 529 (Anthropic overloaded), 503 transitorio, timeouts a los 60s en respuestas largas, streams que se cortan a media generación, y respuestas vacías sin error. Tratarlos con el mismo patrón retry-3-veces-y-rendirse es ingenuo y te va a costar uptime.
La idea es vieja pero crítica con LLMs: si tu provider ya está en mal momento, retries agresivos lo empeoran. El breaker observa fallas en ventana móvil; si pasan un umbral, abre el circuito y rechaza llamadas localmente por un tiempo antes de probar de nuevo con un "canario" en HALF_OPEN.
Configuración que funciona para chatbot en producción:
type State = "CLOSED" | "OPEN" | "HALF_OPEN";
class CircuitBreaker {
private state: State = "CLOSED";
private failures: number[] = [];
private openedAt = 0;
constructor(
private threshold = 5,
private windowMs = 30_000,
private cooldownMs = 60_000,
) {}
async run(fn: () => Promise): Promise {
if (this.state === "OPEN") {
if (Date.now() - this.openedAt < this.cooldownMs) {
throw new Error("circuit_open");
}
this.state = "HALF_OPEN";
}
try {
const result = await fn();
if (this.state === "HALF_OPEN") this.reset();
return result;
} catch (err) {
this.recordFailure();
throw err;
}
}
private recordFailure() {
const now = Date.now();
this.failures = this.failures.filter(t => now - t < this.windowMs);
this.failures.push(now);
if (this.failures.length >= this.threshold || this.state === "HALF_OPEN") {
this.state = "OPEN";
this.openedAt = now;
}
}
private reset() { this.state = "CLOSED"; this.failures = []; }
}
Una instancia POR provider/modelo: el breaker de Opus es distinto del de Sonnet, porque pueden fallar independientemente.
Dentro del breaker, los retries tienen reglas distintas a las APIs REST:
async function withRetry(fn: () => Promise, opts = { maxRetries: 2 }): Promise {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (err) {
const code = (err as any).status;
const retryable = code === 429 || code === 529 || code === 503 || code === 408;
if (!retryable || attempt >= opts.maxRetries) throw err;
const retryAfter = (err as any).headers?.["retry-after"];
const delay = retryAfter
? Number(retryAfter) * 1000
: Math.min(200 * Math.pow(2, attempt) + Math.random() * 100, 3000);
await new Promise(r => setTimeout(r, delay));
attempt++;
}
}
}
El error más caro que vas a cometer: poner retry infinito con backoff "porque eventualmente cede". Si pegaste tu cuota mensual de Anthropic, retries no la abren. Si estás rate-limited por minuto, sí, pero más de 3 retries es señal de que tu volumen rebasa tu plan y necesitas pedir aumento de cuota, no esperar más.
El breaker se combina con el router de tiers: si Opus está en OPEN, el wrapper cae a Sonnet automáticamente. La respuesta es marginalmente peor; el sistema sigue vivo. Sin esta combinación, un mal momento de Anthropic te tira el chatbot completo aunque OpenAI esté sano.
¿Tu chatbot tiene SLO de uptime medido contra el provider, o contra tu propio servicio? Si es contra el provider, la respuesta corta es: necesitas ambos patrones, no opcionalmente.
Artículos relacionados
Costo real Opus 4.7 vs Sonnet 4.6: números 2026
Opus 4.7 cuesta 5x más que Sonnet 4.6 y rinde aproximadamente 2x en razonamiento complejo. Sonnet basta para 70% de los casos productivos. Cuándo el delta paga.
Defensa contra prompt injection: scoring práctico
LLM-as-judge no escala ni en costo ni en latencia. Un scorer regex con patrones ponderados y threshold 0.6 atrapa 90% del problema en 2ms y sin llamadas extras al modelo.
Anatomía de un AI router de cuatro tiers
Gemini Flash para 'gracias', gpt-4o-mini para clasificación, Sonnet 4.6 para conversación útil, Opus 4.7 solo cuando importa. Resultado: 70% menos costo en chatbots de volumen.