← SCRAM AI Lab

Anthropic

Circuit breaker + retry exponencial para LLMs

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

Los LLMs fallan diferente

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.

Circuit breaker: el patrón que sí aplica

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:

  • Umbral: 5 fallas en 30 segundos → OPEN.
  • Cooldown: 60 segundos en OPEN antes de pasar a HALF_OPEN.
  • HALF_OPEN: deja pasar UNA llamada. Si pasa, vuelve a CLOSED. Si falla, otros 60s OPEN.
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.

Retry con backoff exponencial

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++;
    }
  }
}

Lo que NO debes reintentar

  • 400 (bad request): tu prompt es inválido. Reintentar es perder dinero.
  • 401/403: credencial mala. Falla rápido y alerta.
  • 422 (content policy): el modelo se negó. Retry no lo va a convencer.
  • Timeout en streaming a media respuesta: peligroso reintentar; vas a duplicar contenido. Mejor cortar gracefully.

El anti-patrón: retry indefinido en 429

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.

Métricas que importan

  • Estado del circuit breaker por provider, exportado a Prometheus: cuántas veces abrió, duración promedio en OPEN.
  • Distribución de retries: histograma de attempts. Si el p50 es >0, tu provider está degradado.
  • Latencia añadida por retry: el costo invisible. Un retry en backoff de 1.6s en un chatbot mata la fluidez.

Composición con fallback de modelo

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.

resilience
llm
patrones
← Volver a SCRAM AI Lab