Guía10 min

Reintentos, timeouts y rate limits en agentes de IA

Resumen

Cómo reintentar llamadas a OpenAI, Gemini y Claude sin duplicar acciones ni quemar cuota: qué códigos sí (429, 500, 503, 529), cuáles no (400, 401, 403), Retry-After, backoff con jitter, RPM/TPM y el patrón de cola para un agente en producción.

OpenAIGeminiAnthropic
Cola de peticiones con pausas y reintentos frente a un límite de tasa

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

Un agente en producción no falla porque el modelo “se equivoque de tono”. Falla porque la API responde 429, 503 o se queda colgada 40 segundos y tu webhook ya contestó 200. Si reintentas mal, duplicas un cargo, un mensaje o un ticket. Si no reintentas, el usuario ve silencio. Esta guía fija qué códigos se reintentan, con qué espera y dónde vive la cola, con lo que publican OpenAI, Gemini y Claude hoy (3 de septiembre de 2026).

El runtime (Vercel, Workers, VPS) pone el techo de tiempo; eso está en dónde desplegar el agente. Aquí el foco es la política de reintento frente al proveedor del modelo.

La regla de oro: no todos los errores son transitorios

CódigoQué significa (docs)¿Reintentar?
400Request inválido (schema, parámetros). Claude también usa 400 al tocar un spend limit de org/workspaceNo, salvo que hayas cambiado el request
401 / 403Auth o permisosNo. Rota la clave, no el loop
404Recurso inexistenteNo
408 / timeout de clienteTu lado cortóSí, con tope y jitter
429Rate limit, slow_down, o (según proveedor) cuota/crédito agotadoA veces. Lee error.code y Retry-After
500Error interno del proveedorSí, backoff corto
503Modelo sobrecargado (OpenAI: server_is_overloaded)Sí, respeta Retry-After
529Claude overloaded_error (tráfico alto global)Sí, backoff. No es tu RPM

Si no distingues un 429 de cuota agotada de un 429 de RPM, vas a reintentar hasta vaciar el bolsillo o hasta que el proveedor te corte. OpenAI documenta 429 distintos: credit_balance_exhausted (no hay créditos prepaid) frente a “rate limit reached” y slow_down. El primero se resuelve recargando, no esperando 2 segundos.

Separación entre errores permanentes y transitorios en la cola de un agente

OpenAI: Retry-After, jitter y el SDK

OpenAI mide RPM, RPD y TPM. Pasarte cualquiera de los tres dispara 429. Los límites son por organización, no por usuario: el compañero que corre evals a las 9 AM comparte tu techo.

Qué hacer, según sus propias docs:

  1. Si llega Retry-After, úsalo. Los SDK oficiales ya lo honran en los reintentos elegibles.
  2. Si no llega header, exponential backoff con jitter y un tope de reintentos (3–5 es suficiente para un webhook).
  3. 500: espera breve y reintenta; si persiste, mira status.openai.com.
  4. 503 (server_is_overloaded): otra vez Retry-After si existe.

No copies un sleep(2 ** n) sin jitter: mil agentes haciendo lo mismo a los 2, 4, 8 segundos recrean la sobrecarga. El jitter (espera aleatoria dentro de un rango) es la diferencia entre un retry útil y un DDoS a ti mismo.

Gemini: RPM, TPM, RPD por proyecto

Gemini documenta tres ejes: requests per minute, tokens per minute (input) y requests per day. El ejemplo oficial es brutalmente simple: RPM 20 y haces 21 en un minuto → error, aunque te sobre TPM. Los límites van por proyecto, no por API key. El RPD se resetea a medianoche hora del Pacífico.

El error es 429 RESOURCE_EXHAUSTED. La receta oficial:

  • Esperar y reintentar un rato.
  • Bajar el costo de cada request (menos contexto, menos output).
  • Si lo pegas en uso normal, pedir un aumento de límite.

No inventes números de RPM en el código: leelos del proyecto (AI Studio / Cloud) y trátalos como config. Un hardcode de “60 RPM” queda obsoleto en el siguiente tier.

Claude: 429, 529 y el request_id

Claude separa spend limits (techo mensual de dinero) y rate limits (techo de requests en una ventana). Un 429 puede ser rate limit o haber tocado el spend cap / el límite del workspace de Claude Code. Un 400 también puede ser spend limit a nivel org. Si reintentas un spend cap, solo alargas el dolor: el header retry-after sigue fallando hasta que haya cuota otra vez.

Códigos que sí son transitorios:

  • 500 api_error: backoff exponencial; si no cede, manda el request_id a soporte. Claude lo incluye en el JSON de error.
  • 529 overloaded_error: tráfico alto de todos los usuarios, no tu RPM. El SDK oficial reintenta algunos de estos solo.

Subir de golpe el tráfico puede dar 429 por acceleration limits aunque tu tier “alcance”. La doc pide ramp-up gradual. Un cron que dispara 200 agentes a las 00:00 es el anti-patrón.

El patrón que sí funciona en un agente

El webhook del canal (Telegram, WhatsApp, Slack) suele exigir 200 rápido. No reintentes la inferencia dentro de ese request: si el modelo tarda o 429, te comes el timeout de la plataforma y además reintentas a ciegas.

Patrón mínimo:

  1. Validar firma. Encolar el trabajo. Responder 200.
  2. Un worker toma el job con un idempotency key (id del mensaje / event id).
  3. Llamar al modelo con timeout de cliente (30–60 s según el modelo y el max_tokens).
  4. Si el error es transitorio: el job vuelve a la cola con delay (Retry-After o backoff+jitter). Tope de 3–5 intentos.
  5. Si el error es permanente (400/401/403, cuota agotada): marcar el job como dead letter, alertar, no loop.
  6. Si la tool del agente tiene efecto (cobrar, enviar mail, crear ticket): la tool también es idempotente. Reintentar el modelo no debe reenviar el mail.

Cola de jobs con backoff, tope de intentos y salida a dead letter

Eso encaja con la arquitectura de webhooks, colas y memoria. El reintento vive en la cola, no en un for dentro del handler.

Timeouts: corta tú, no el usuario

Un timeout de cliente más largo que el del host (función serverless, reverse proxy) es inútil: el host mata primero. Alinea:

  • Timeout del HTTP client < timeout de la función/proceso.
  • max_tokens y tools acotados para que una corrida quebe en ese presupuesto.
  • Si el agente necesita más tiempo, no alargues el request: parte el trabajo (plan → pasos) o muévelo a un runtime largo (Docker en VPS).

Un timeout debe ser reintentable solo si la operación es idempotente. Si ya ejecutaste charge_card y el POST se colgó antes de leer la respuesta, reintentar cobra dos veces. Guarda el id de la tool call.

Checklist

  • Clasificas 400/401/403 como no-retry; 500/503/529 como retry; 429 según error.code + Retry-After.
  • Hay tope de intentos (3–5) y jitter. Nada de while true.
  • El webhook no reintenta inferencia: encola y sale.
  • Idempotency key por mensaje/evento. Tools con efecto también.
  • Logs con status, error.code, request_id (Claude) y número de intento, para observabilidad.
  • Alertas distintas: “cuota agotada” ≠ “RPM”.
  • Límites leídos del proyecto (RPM/TPM/RPD), no hardcodeados.
  • Un eval de 10 fallos simulados (429, 500, 400) antes de producción.

FAQ

¿El SDK ya reintenta, para qué una cola? El SDK reintenta la misma llamada HTTP. No protege el webhook, no hace idempotencia de tools y no te saca de un spend cap. Úsalo como primera red; la cola es la segunda.

¿Cuántos reintentos? Tres es el default razonable. Cinco si el job es barato y el 503 es frecuente. Más de eso es un loop de costo.

¿Puedo bajar de modelo al 429? Sí como fallback explícito (más barato / más cuota), no como retry silencioso del mismo request. Documenta el fallback: el usuario y tus evals deben saber que cambió el modelo.

¿Retry en function calling? Reintenta la llamada al modelo. No reejecutes la tool a menos que sea idempotente o que tu código confirme que no corrió.

Empieza por loguear status + error.code una semana. La política de retry se escribe con esos números, no con un tutorial genérico de backoff.