Resiliencia frente a saturación de LLMs: lectura adaptativa de cabeceras 429 con jitter y Circuit Breakers de control
Mitigación de congestión en proveedores de inferencia, evasión del problema Thundering Herd y aislamiento preventivo.
"En ingeniería de redes, el control de congestión es un principio fundacional: Van Jacobson evitó el colapso de Internet en 1988 introduciendo algoritmos de evasión de congestión en TCP basados en señales de red y desincronización probabilística. La saturación de un proveedor externo no se resuelve mediante reintentos ciegos: se gestiona respetando las cabeceras del canal, desincronizando a los clientes mediante Jitter uniforme para mitigar la manada atronadora (Thundering Herd), y desacoplando el tráfico preventivamente con un Circuit Breaker antes de agotar los recursos de la propia infraestructura."
1. Resumen Ejecutivo: La Nueva Dinámica de Saturación de los LLMs
Los modelos de lenguaje de gran tamaño (LLMs) son proveedores externos con dinámicas de saturación sustancialmente distintas a las de las APIs transaccionales convencionales. A diferencia de un endpoint estándar —cuyos límites suelen medirse en peticiones por segundo (RPS)—, los proveedores de inferencia aplican políticas multidimensionales: cuotas de tokens por minuto (TPM), tokens por día (TPD) y concurrencia máxima por clúster.
Cuando un sistema de agentes experimenta ráfagas de tráfico, la respuesta por defecto de muchas librerías consiste en aplicar reintentos ciegos con retroceso exponencial (Exponential Backoff). Esta estrategia es contraproducente en IA: si decenas de procesos concurrentes reciben un código HTTP 429 Too Many Requests y calculan su reintento con la misma fórmula determinista, se sincronizan involuntariamente, generando una segunda oleada coordinada (Thundering Herd Problem) que satura nuevamente el servicio. Asimismo, si el proveedor experimenta una indisponibilidad generalizada, los reintentos descontrolados saturan los workers y agotan los recursos de la plataforma.
En este artículo abordamos la estrategia integral de resiliencia implementada en javiergalvez-ia: lectura adaptativa y parseo semántico de cabeceras del proveedor (Retry-After, x-ratelimit-reset-tokens, con soporte para formatos como 1m30s o 500ms), desincronización probabilística mediante Full Jitter, suspensión asíncrona no bloqueante en el Event Loop de FastAPI (asyncio.sleep), y una máquina de estados determinista de Circuit Breaker (CLOSED → OPEN → HALF_OPEN → CLOSED) en el Control Plane (jg_dashboard) para cortar el tráfico de forma preventiva antes de agotar los recursos del sistema.
2. Puntos de Fricción: Reintentos Ciegos y la Manada Atronadora
Al monitorizar sistemas de IA en momentos de estrés, se aprecian con frecuencia las siguientes fallas de resiliencia:
-
Ignorar las Pistas Explícitas del Proveedor (Cabeceras 429): Los proveedores de LLMs suelen informar con precisión el tiempo de espera recomendado en la cabecera
Retry-Aftero en cabeceras propietarias comox-ratelimit-reset-requests. Ignorar estas cabeceras y utilizar pausas estáticas arbitrarias garantiza que el siguiente reintento colisionará con la ventana de rate-limiting activa. -
El Problema de la Manada Atronadora (Thundering Herd): Cuando múltiples agentes concurrentes comparten la misma cuota de tokens y son limitados simultáneamente, reintentar en intervalos deterministas genera pulsos de tráfico coordinados que atrapan al sistema en un ciclo continuo de saturación.
-
Ausencia de Cortafuegos de Fallo (Circuit Breakers): Si el proveedor de inferencia experimenta una indisponibilidad prolongada (devolviendo errores 500 o 429 persistentes), las aplicaciones sin Circuit Breaker continúan despachando tráfico a ciegas, acumulando peticiones en cola, agotando la memoria de los workers y degradando globalmente la experiencia de usuario.
3. Arquitectura y Solución Técnica: Jitter Desincronizador y Máquina de Estados
La estrategia de resiliencia se articula en dos niveles de defensa: manejo adaptativo a nivel de cliente de inferencia (jg_agente) y protección perimetral con Circuit Breaker en el plano de control (jg_dashboard).
Diagrama de la Máquina de Estados del Circuit Breaker
┌────────────────────────────────────────────────────────┐
│ ESTADO: CLOSED │
│ (Operación normal: Las peticiones pasan al LLM) │
└───────────────────────────┬────────────────────────────┘
│
Fallos consecutivos >= Umbral
(p. ej. 5 errores 429 / 5xx)
│
▼
┌────────────────────────────────────────────────────────┐
│ ESTADO: OPEN │
│ (Circuito Abierto: Peticiones cortadas al instante) │
│ - Lanza CircuitBreakerOpenException │
│ - Enruta inmediatamente hacia fallback alternativo │
└───────────────────────────┬────────────────────────────┘
│
Cooldown Period expirado
(p. ej. transcurridos 60 segundos)
│
▼
┌────────────────────────────────────────────────────────┐
│ ESTADO: HALF_OPEN │
│ (Sonda de prueba: Se permite 1 petición piloto) │
└─────────────┬────────────────────────────┬─────────────┘
│ │
Sonda falla con error Sonda responde OK (200)
│ │
▼ ▼
Vuelve a OPEN Vuelve a CLOSED
(Nuevo Cooldown) (Circuito Saneado)
Principios Fundamentales del Diseño:
-
Parseo Semántico Multiformato de
Retry-After: Las APIs devuelven tiempos de espera en múltiples variantes: segundos enteros ("30"), marcas de tiempo HTTP Date, milisegundos ("500ms") o representaciones textuales ("1m30s"). El parser dejg_agentenormaliza cualquier formato a un valor flotante en segundos. -
Inyección de Jitter Uniforme: Para evitar la sincronización de agentes, al tiempo de espera derivado de la cabecera se le suma una componente aleatoria uniforme distribuida en el intervalo $[0.1, 0.5]$ segundos: $$T_{\text{wait}} = T_{\text{header}} + \text{random}(0.1, 0.5)$$
-
Suspensión No Bloqueante en el Event Loop: La pausa se realiza mediante
await asyncio.sleep(). Esto cede voluntariamente el control al Event Loop de Python, permitiendo que otros hilos y peticiones no bloqueadas sigan procesándose sin penalización de CPU ni retención de threads del sistema operativo. -
Circuito Cortado Preventivo en el Dashboard:
CircuitBreakerServicemonitoriza la tasa de fallos de los agentes. Si un modelo supera el umbral de errores consecutivos, el circuito conmuta aOPEN, cortando las peticiones en milisegundos y activando respuestas de contingencia locales sin sobrecargar el servicio aguas arriba.
4. Código Fuente Comentado
Parser Adaptativo de Duraciones y Jitter en jg_agente
# jg_agente/app/core/llm.py
import re
import random
import asyncio
from typing import Optional
def parse_duration_seconds(val: str) -> Optional[float]:
"""Normaliza cadenas de tiempo complejas (1m30s, 500ms, 45) a segundos flotantes."""
val = val.strip().lower()
# Formato simple en segundos
try:
return float(val)
except ValueError:
pass
# Formato en milisegundos (500ms)
if val.endswith("ms"):
try:
return float(val[:-2]) / 1000.0
except ValueError:
pass
# Formato combinado minutos y segundos (1m30s)
match = re.match(r"(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?", val)
if match and any(match.groups()):
mins = float(match.group(1) or 0)
secs = float(match.group(2) or 0)
return (mins * 60.0) + secs
return None
async def wait_for_llm_rate_limit(retry_after_header: Optional[str] = None):
"""Calcula el tiempo de espera adaptativo con jitter uniforme desincronizador."""
base_wait = 1.0
if retry_after_header:
parsed = parse_duration_seconds(retry_after_header)
if parsed is not None and parsed > 0:
base_wait = parsed
# Adición de Jitter Uniforme [0.1, 0.5] segundos contra Thundering Herd
jitter = random.uniform(0.1, 0.5)
total_wait = base_wait + jitter
# Suspensión asíncrona no bloqueante
await asyncio.sleep(total_wait)
Máquina de Estados del Circuit Breaker en Laravel 12 (jg_dashboard)
// jg_dashboard/app/Services/Resilience/CircuitBreakerService.php
namespace App\Services\Resilience;
use Illuminate\Support\Facades\Cache;
use Closure;
class CircuitBreakerService
{
const STATE_CLOSED = 'CLOSED';
const STATE_OPEN = 'OPEN';
const STATE_HALF_OPEN = 'HALF_OPEN';
public function __construct(
protected string $serviceName,
protected int $failureThreshold = 5,
protected int $cooldownSeconds = 60
) {}
public function execute(Closure $action, ?Closure $fallback = null)
{
$state = $this->getState();
// 1. Si el circuito está abierto, evaluar si ha expirado el cooldown
if ($state === self::STATE_OPEN) {
if ($this->hasCooldownExpired()) {
$this->transitionTo(self::STATE_HALF_OPEN);
} else {
if ($fallback) {
return $fallback();
}
throw new CircuitBreakerOpenException("El circuito para '{$this->serviceName}' está ABIERTO");
}
}
// 2. Ejecución de la acción protegida
try {
$result = $action();
$this->recordSuccess();
return $result;
} catch (\Throwable $e) {
$this->recordFailure();
if ($fallback) {
return $fallback();
}
throw $e;
}
}
protected function recordSuccess(): void
{
// Si estábamos en HALF_OPEN y la sonda tuvo éxito, sanamos el circuito
if ($this->getState() === self::STATE_HALF_OPEN) {
$this->transitionTo(self::STATE_CLOSED);
$this->resetFailures();
}
}
protected function recordFailure(): void
{
$failures = $this->incrementFailures();
// Si falla en HALF_OPEN o supera el umbral en CLOSED, abrimos el circuito
if ($this->getState() === self::STATE_HALF_OPEN || $failures >= $this->failureThreshold) {
$this->transitionTo(self::STATE_OPEN);
$this->recordTripTimestamp();
}
}
}
5. Consideraciones de Producción, Trade-offs y Límites
-
Evitar la Ocultación Silenciosa de Fallos en Fallbacks: Cuando el Circuit Breaker conmuta a un fallback (por ejemplo, devolver un resultado heurístico precalculado o un modelo local más pequeño), es fundamental registrar métricas de alerta en el stack de observabilidad y notificar explícitamente en el contrato de respuesta que el resultado se generó en modo degradado.
-
Sincronización de Estado en Entornos Horizontales Multi-Instancia: En un clúster horizontal con múltiples réplicas de
jg_dashboard, el estado del Circuit Breaker (state,failures,tripped_at) debe residir en una memoria distribuida (Redis). Esto garantiza que la apertura del circuito se propague de forma atómica a todas las instancias en lugar de requerir que cada nodo experimente la cascada de fallos de forma independiente. -
Calibración Crítica de Umbrales del Circuit Breaker: Ajustar los umbrales de disparo implica balancear dos riesgos operacionales opuestos:
- Umbrales excesivamente estrictos (p. ej. 2 fallos consecutivos): Un micro-corte de red transitorio o dos peticiones aisladas que coincidan con un reset de cuota abrirán el circuito de forma prematura. Esto genera falsos positivos y degrada innecesariamente el servicio hacia fallbacks cuando el proveedor sigue en realidad disponible.
- Umbrales excesivamente permisivos (p. ej. 20 fallos o ventanas temporales dilatadas): Si el proveedor colapsa, el sistema tardará demasiado en reaccionar. Las peticiones se acumularán en el pool de conexiones, saturarán la memoria del Event Loop y bloquearán las colas de procesamiento antes de que el circuito logre cortar el flujo.
- Recomendación de arquitectura: Para cargas heterogéneas, se recomienda un umbral de 5 fallos consecutivos en una ventana móvil de 30 segundos, con un período de enfriamiento (cooldown) de 60 segundos antes de pasar a
HALF_OPEN.
-
Gestión de Cuotas por Usuario vs. Cuotas de Proveedor: El parseo de cabeceras 429 protege la infraestructura frente a los límites globales del proveedor de LLM. Sin embargo, para prevenir que un único usuario consuma toda la cuota de la organización, debe combinarse con un Token Bucket o Leaky Bucket perimetral que asigne límites individuales por usuario o inquilino (tenant).
6. Reflexión de Arquitectura: Saber cuándo detenerse
En el diseño de sistemas distribuidos, insistir a ciegas ante un servicio que reporta saturación no constituye una muestra de resiliencia, sino una omisión de la teoría de colas.
Construir plataformas de inteligencia artificial para producción requiere rigor sistémico: asumir que los proveedores externos experimentarán fallos y límites de capacidad, y diseñar el software para amortiguar las esperas con rigor probabilístico (Jitter) y desacoplar la carga mediante Circuit Breakers cuando la tasa de error lo justifique. Un circuito que interrumpe una llamada en 2 milisegundos y deriva a una alternativa de contingencia preserva la estabilidad de la plataforma y protege la experiencia del usuario.
7. Enlaces y Referencias en el Repositorio
- Módulo de Inferencia LLM y Parser de Reintentos:
jg_agente/app/core/llm.py - Suite de Pruebas de Retries y Jitter:
jg_agente/tests/test_llm_retries.py - Servicio de Circuit Breaker en Laravel:
jg_dashboard/app/Services/Resilience/CircuitBreakerService.php - Excepción de Circuito Abierto:
jg_dashboard/app/Services/Resilience/CircuitBreakerOpenException.php - Tests Unitarios del Circuit Breaker:
jg_dashboard/tests/Unit/Services/CircuitBreakerServiceTest.php
| Título | Resiliencia frente a saturación de LLMs: lectura adaptativa de cabeceras 429 con jitter y Circuit Breakers de control |
|---|---|
| Archivo | docs/02_jg_agent/articulos/11_rate_limiting_resilience_circuit_breakers.md |
| Descripción | Mitigación de congestión en proveedores de inferencia, evasión del problema Thundering Herd y aislamiento preventivo. |
| Autor | Javier Gálvez |
| Fecha | 2026-10-17 |
| Etiquetas |
#Artículos
#Arquitectura
#Resiliencia
#Rate Limiting
#429 Too Many Requests
#Circuit Breakers
#Jitter
#Control de Congestión
|