Error API 429 Too Many Requests: límites, Retry-After y backoff seguro
Guía práctica para resolver errores HTTP 429 en API de LLM: análisis de límites RPM/TPM, lectura de Retry-After e implementación de backoff con jitter.
Índice
Error API 429 Too Many Requests: límites, Retry-After y backoff seguro
El error HTTP 429 Too Many Requests ocurre cuando un cliente supera los límites de frecuencia o el volumen de tokens establecidos por el proveedor de la API. Ejecutar reintentos infinitos sin control no soluciona el problema, sino que agrava el bloqueo generando tormentas de reintentos (retry storms).
Para restablecer la estabilidad de su aplicación, es necesario identificar con precisión el tipo de límite (RPM, TPM o saldo agotado), analizar correctamente el encabezado Retry-After e implementar un algoritmo de retroceso exponencial con ruido aleatorio (Full Jitter Exponential Backoff).
Por qué ocurre HTTP 429: anatomía de los límites
En las API de modelos de lenguaje modernos (OpenAI, Anthropic y pasarelas compatibles), el código de estado 429 se genera mediante tres mecanismos principales:
- RPM (Requests Per Minute): límite en la cantidad de solicitudes HTTP por minuto. Aparece al ejecutar múltiples procesos paralelos sin colas de control.
- TPM (Tokens Per Minute): límite en el volumen acumulado de tokens de entrada y salida en una ventana móvil de un minuto. Se activa con frecuencia al enviar contextos extensos.
- Agotamiento de cuota o saldo: bloqueo debido a depósito cero, límite estricto de gasto alcanzado o finalización del saldo prepagado.
HTTP/1.1 429 Too Many Requests
Date: Sun, 23 Aug 2026 03:00:00 GMT
Content-Type: application/json
Retry-After: 6
x-ratelimit-limit-requests: 500
x-ratelimit-remaining-requests: 0
x-ratelimit-reset-requests: 6s
x-ratelimit-limit-tokens: 30000
x-ratelimit-remaining-tokens: 1200
x-ratelimit-reset-tokens: 150ms
{
"error": {
"message": "Rate limit reached for model in organization on tokens per minute (TPM). Please try again in 6s.",
"type": "tokens",
"param": null,
"code": "rate_limit_exceeded"
}
}
Cuando el error se debe a la falta de saldo o a límites rígidos de suscripción, los reintentos automáticos solo desperdician recursos de red. Para aislar de inmediato la causa sin descifrar registros complejos, BetterToken ofrece un panel de control transparente: muestra en tiempo real los estados HTTP de cada solicitud, el desglose exacto de tokens de entrada, salida y caché, y el saldo disponible bajo el modelo pay-as-you-go sin bloqueos de 5 horas.
Matriz de diagnóstico del error 429
| Síntoma | Causa principal | Inspección de respuesta | Solución técnica |
|---|---|---|---|
| Fallo en ráfagas paralelas | Límite RPM excedido | x-ratelimit-remaining-requests: 0 | Limitar concurrencia con semáforos o colas |
| Fallo en prompts extensos | Límite TPM excedido | x-ratelimit-remaining-tokens < tamaño del prompt | Optimizar contexto, habilitar prompt caching o dividir llamadas |
| 100% de llamadas devuelven 429 | Saldo / Cuota agotada | Código insufficient_quota o mensaje de facturación | Detener reintentos y recargar saldo o revisar permisos |
| Crecimiento en avalancha de 429 | Tormenta de reintentos | Encabezados Retry-After ignorados por los workers | Añadir ruido aleatorio (Jitter) al cálculo del retardo |
Cómo interpretar el encabezado Retry-After
La especificación RFC 6585 establece dos formatos válidos para el encabezado Retry-After:
- Segundos relativos (número entero o decimal, por ejemplo
Retry-After: 12); - Fecha HTTP (marca de tiempo GMT, por ejemplo
Retry-After: Sun, 23 Aug 2026 03:05:00 GMT).
import datetime
import email.utils
import time
def parse_retry_after(header_value: str | None, default_delay: float = 1.0) -> float:
if not header_value:
return default_delay
header_value = header_value.strip()
try:
return max(0.0, float(header_value))
except ValueError:
pass
try:
parsed_date = email.utils.parsedate_to_datetime(header_value)
now = datetime.datetime.now(datetime.timezone.utc)
delay = (parsed_date - now).total_seconds()
return max(0.0, delay)
except Exception:
return default_delay
Implementación de Full Jitter Exponential Backoff
Si no existe el encabezado Retry-After, la solución estándar es el retroceso exponencial con variación aleatoria total (Full Jitter). La fórmula para el intento $i$ es:
$$T_{ ext{wait}} = ext{random}(0, \min(T_{ ext{max}}, T_{ ext{base}} imes 2^i))$$
import asyncio
import json
import random
import httpx
class RateLimitRetryClient:
def __init__(
self,
base_url: str = "https://www.bettertoken.ai/v1",
api_key: str = "",
max_retries: int = 4,
base_delay: float = 1.0,
max_delay: float = 32.0,
):
self.base_url = base_url
self.api_key = api_key
self.max_retries = max_retries
self.base_delay = base_delay
self.max_delay = max_delay
self.client = httpx.AsyncClient(
base_url=self.base_url,
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=60.0,
)
async def send_chat_completion(self, payload: dict) -> dict:
for attempt in range(self.max_retries + 1):
try:
response = await self.client.post("/chat/completions", json=payload)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
error_data = response.json().get("error", {})
error_code = error_data.get("code")
if error_code in ("insufficient_quota", "billing_not_active"):
raise RuntimeError(f"Error de facturación: {error_data.get('message')}")
if attempt == self.max_retries:
raise RuntimeError(f"Límite de reintentos agotado (429): {response.text}")
retry_after = response.headers.get("Retry-After")
if retry_after:
wait_time = parse_retry_after(retry_after) + random.uniform(0.1, 0.5)
else:
backoff_cap = min(self.max_delay, self.base_delay * (2 ** attempt))
wait_time = random.uniform(0, backoff_cap)
await asyncio.sleep(wait_time)
continue
response.raise_for_status()
except httpx.RequestError as exc:
if attempt == self.max_retries:
raise
wait_time = min(self.max_delay, self.base_delay * (2 ** attempt))
await asyncio.sleep(wait_time)
raise RuntimeError("La solicitud falló tras agotar el presupuesto de reintentos")
Idempotencia y seguridad en reintentos
Reintentar operaciones de lectura (GET) es seguro. Sin embargo, al invocar inferencia de LLM mediante POST:
- Evite duplicar tareas generativas: si una solicitud se interrumpe por tiempo de espera, verifique si se consumieron tokens antes de volver a enviarla.
- Utilice identificadores de cliente: incluya encabezados
X-Request-IDúnicos para auditar llamadas en los registros. - No trate errores 401 o 403 como 429: los errores de autenticación requieren corregir las credenciales, no aplicar pausas.
Validación y recuperación del servicio
Antes de restaurar el tráfico masivo:
- Envíe una solicitud de prueba mínima (
max_tokens: 5). - Compruebe la recepción de HTTP 200 y el encabezado
x-ratelimit-remaining-requests. - Aumente la concurrencia gradualmente mientras supervisa la tasa de errores 429 en sus métricas.
Para evitar bloqueos 429 inesperados debidos a límites estrictos por minuto y supervisar de forma transparente el estado de cada llamada, acceda a BetterToken API, genere claves de API dedicadas y controle el consumo de tokens en tiempo real desde el panel de control.