cd ../back to blog
$Guide//June 4, 2026//7 min read

Límites de tasa, reintentos y backoff: manejo de errores de nivel producción para API de IA

Una guía práctica para manejar errores de API de IA en producción: reintenta solo 429 y 5xx, backoff exponencial con jitter, respeta Retry-After, timeouts, idempotencia y circuit breakers.

La demo funcionó a la primera llamada. La producción son las otras 999.999 — las que topan con un límite de tasa durante un pico de tráfico, atrapan un 503 aguas arriba a mitad de despliegue o se cuelgan en un socket que nunca responde. La diferencia entre una integración de juguete y una fiable está casi por completo en cómo gestionas el camino infeliz: qué reintentas, cuánto esperas y cuándo te rindes. Hazlo mal y un breve tropiezo del proveedor se convierte en una caída autoinfligida, mientras cada uno de tus workers reintenta al unísono perfecto y termina el trabajo que empezó el limitador de tasa.

Este es un recorrido práctico y de nivel producción: clasifica los errores para reintentar solo lo reintentable, aplica backoff exponencial con jitter, respeta Retry-After, fija timeouts sensatos, haz seguros los reintentos con idempotencia y deja de machacar una dependencia muerta con un circuit breaker. El código es Python con el SDK de OpenAI contra https://api.brievio.com/v1, pero las reglas son las mismas en cualquier cliente HTTP y cualquier lenguaje.

Reintenta lo reintentable — y nada más

El bug más común con diferencia en el manejo de errores de las API de IA es reintentar cosas que nunca tendrán éxito. Un 401 (clave mala), un 400 (petición mal formada), un 422 (tu prompt es demasiado largo) — son deterministas. El segundo intento falla exactamente igual que el primero, salvo que ahora son cinco intentos y varios segundos después. Peor aún: has escondido un bug real detrás de un bucle de reintentos. El único 4xx que vale la pena reintentar es 429 (límite de tasa), porque ese sí es transitorio.

  • Reintenta: 429, y 500 / 502 / 503 / 504 — son condiciones transitorias del lado del servidor o de capacidad.
  • Reintenta: errores de conexión y timeouts de lectura (la petición quizá nunca llegó al modelo, o el modelo respondió hacia un socket cerrado).
  • Nunca reintentes: cualquier otro 4xx 400, 401, 403, 404, 422. Sácalos a la luz, alerta sobre ellos, arregla al que llama.

Un instinto útil: un reintento es una apuesta a que la misma petición obtendrá una respuesta distinta. Eso solo es cierto cuando el fallo fue por tiempo o capacidad, no por la petición en sí.

Backoff exponencial con jitter

Una vez que sabes que un fallo es reintentable, la pregunta es cuánto esperar. Reintentar de inmediato no sirve de nada — la condición que causó el 429 sigue ahí un milisegundo después. La respuesta estándar es el backoff exponencial: espera aproximadamente 0.5s, luego 1s, 2s, 4s, duplicando en cada intento, con un tope para que una recuperación larga no te bloquee para siempre.

Pero el backoff exponencial puro tiene un modo de fallo perverso a escala. Si 500 workers reciben el límite de tasa en el mismo instante — que es justo lo que pasa durante un pico — todos hacen backoff la misma cantidad y reintentan en el mismo instante, recreando el pico con un reloj de 2 segundos. Esto es la estampida (thundering herd). El arreglo es el jitter: aleatoriza cada espera para que los reintentos se repartan. El jitter completo (dormir una cantidad aleatoria entre cero y el tope del backoff) desincroniza la estampida mucho mejor que añadir un pequeño empujón aleatorio a una espera fija.

retry.py
# Un wrapper de reintentos que de verdad puedes desplegar. Dos reglas hacen casi todo el trabajo:
#   1. Reintenta solo lo reintentable (429 + 5xx + errores de conexión). Nunca 400/401/422.
#   2. Aplica backoff exponencial Y añade jitter, o cada cliente reintenta al unísono.
import random
import time

from openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutError

client = OpenAI(
    api_key="sk-brievio-...",
    base_url="https://api.brievio.com/v1",
    timeout=30,          # tope estricto por petición — ver "Timeouts" más abajo
)

RETRYABLE_STATUS = {429, 500, 502, 503, 504}
MAX_ATTEMPTS = 5
BASE_DELAY = 0.5         # segundos
MAX_DELAY = 20.0         # limita el backoff para que una recuperación lenta no se eternice

def chat_with_retry(**kwargs):
    for attempt in range(MAX_ATTEMPTS):
        try:
            return client.chat.completions.create(**kwargs)
        except APIStatusError as e:
            # Un 4xx que no sea 429 es TU bug (parámetros erróneos, clave mala, demasiado largo).
            # Reintentarlo solo quema latencia — falla rápido.
            if e.status_code not in RETRYABLE_STATUS:
                raise
            last_error = e
        except (APIConnectionError, APITimeoutError) as e:
            # Microcorte de red o saltó nuestro timeout. Es seguro reintentar una lectura.
            last_error = e

        if attempt == MAX_ATTEMPTS - 1:
            break

        # Backoff exponencial con jitter COMPLETO: sleep ∈ [0, base * 2**attempt].
        # El jitter completo (no "backoff + pequeño aleatorio") es lo que de verdad
        # desincroniza una estampida. Ver el AWS Architecture Blog sobre backoff con jitter.
        ceiling = min(MAX_DELAY, BASE_DELAY * (2 ** attempt))
        time.sleep(random.uniform(0, ceiling))

    raise last_error

Fíjate en el tope de intentos (MAX_ATTEMPTS = 5) y el tope de espera (MAX_DELAY). Los reintentos ilimitados son la forma de que un tropiezo transitorio se convierta en un backlog que nunca se drena: las peticiones se acumulan más rápido de lo que se despachan, la latencia sube y los llamantes aguas arriba agotan su tiempo y reintentan sus peticiones también. Acota ambos y deja que el fallo sea visible en vez de quedar amortiguado.

Respeta Retry-After — no adivines

Tu curva de backoff es una conjetura sobre cuándo se libera la capacidad. Cuando el servidor te da la respuesta directamente, úsala. Un 429 suele llevar una cabecera Retry-After (delta-segundos o una fecha HTTP) que refleja la ventana real de reset. Dormir ese valor es estrictamente mejor que cualquier fórmula, porque es la verdad de campo y no una heurística.

retry_after.py
# Cuando el servidor te dice cuánto esperar, hazle caso. Un 429 (y a veces un
# 503) lleva una cabecera Retry-After. Respetarla supera cualquier curva de backoff
# que inventes, porque refleja la ventana real de reset — no una conjetura.
import email.utils as eut
import time

def retry_delay(resp_headers, attempt, base=0.5, cap=20.0):
    # 1. Prioriza la instrucción del servidor.
    ra = resp_headers.get("retry-after")
    if ra is not None:
        try:
            return float(ra)                       # forma delta-segundos: "2"
        except ValueError:
            when = eut.parsedate_to_datetime(ra)   # forma fecha-HTTP
            return max(0.0, when.timestamp() - time.time())

    # 2. ¿Sin cabecera? Recurre al backoff exponencial con jitter completo.
    import random
    return random.uniform(0, min(cap, base * (2 ** attempt)))

# Notas:
#   - Algunos proveedores también envían X-RateLimit-Reset; trátalo igual.
#   - Añade un suelo mínimo (p. ej. 50ms) para que un "Retry-After: 0" no entre en bucle.
#   - Brievio expone Retry-After en el 429 en vez de estancar el socket en silencio,
#     así que esta ruta es alcanzable — puedes hacer backoff de verdad sobre la señal.

Esto solo funciona si el gateway de verdad devuelve la cabecera en vez de absorber el límite y estancar tu socket noventa segundos. Brievio falla rápido y a la vista — un límite de tasa vuelve como un 429 limpio con Retry-After, no como una conexión colgada — así que la ruta de «escuchar al servidor» es alcanzable. La taxonomía completa de errores, con qué códigos llevan qué cabeceras, está en la referencia de errores.

Timeouts: el modo de fallo que nadie prueba

Una política de reintentos es inútil si la petición nunca vuelve para ser reintentada. Una llamada de IA sin timeout, tarde o temprano, se colgará — una conexión semiabierta, un upstream atascado, un balanceador que descartó el flujo. Sin un plazo, esa única petición retiene un worker, una conexión y un hueco en cada cola que tenga detrás hasta que el SO se rinde minutos más tarde. Fija un timeout explícito por petición (el timeout=30 de arriba) para que una llamada atascada se convierta en un APITimeoutError que puedes reintentar, en vez de en peso muerto.

  • El timeout por petición acota un único intento. Para streaming, el presupuesto que importa es el time-to-first-token más un timeout de inactividad entre chunks, no un total de reloj de pared.
  • El plazo global acota toda la secuencia de reintentos. Lleva un presupuesto (p. ej. 60s de extremo a extremo) y deja de reintentar cuando se agote — quien te espera tiene su propio plazo.
  • Timeout > p99 esperado, no p50. Fíjalo justo por encima de tu latencia de cola real. Demasiado ajustado y cancelarás peticiones buenas que estaban a punto de tener éxito, fabricando carga por impaciencia.

Idempotencia: hacer seguros los reintentos

Los reintentos introducen un peligro sutil. Cuando una petición agota su tiempo, no sabes si el servidor la procesó — tu lectura de la respuesta falló, pero el trabajo puede haberse completado. Reintenta a ciegas y puedes cobrar dos veces a un cliente, enviar una notificación dos veces o escribir una fila duplicada. Las lecturas son seguras de reintentar por naturaleza. Los efectos secundarios no.

La defensa es una clave de idempotencia: un ID único que asocias a una operación lógica para que el servidor (o tu propio handler) colapse los duplicados. Para las llamadas de inferencia puras normalmente no hay efecto secundario externo del que preocuparse — pero en cuanto una terminación dispara una escritura en BD, un pago o un mensaje saliente, genera una clave estable por unidad lógica de trabajo y deduplica sobre ella. La regla práctica: si un reintento pudiera ocurrir dos veces, diséñalo como si fuera a ocurrir.

Circuit breakers: deja de patear una dependencia muerta

El backoff gestiona una única petición que batalla. No hace nada por una caída sostenida — si un upstream está caído del todo durante dos minutos, cada petición recorre su escalera completa de reintentos, espera el máximo y falla igualmente, mientras tu latencia y la profundidad de cola se disparan. Un circuit breaker cortocircuita esto: tras N fallos consecutivos se «abre» y rechaza de inmediato las nuevas llamadas (o las enruta a un fallback) durante una ventana de enfriamiento, y luego deja pasar una única sonda para probar la recuperación antes de cerrarse de nuevo.

  • Cerrado: operación normal, las peticiones fluyen, los fallos se cuentan.
  • Abierto: umbral disparado — rechaza rápido durante un periodo de enfriamiento en vez de apilar reintentos condenados.
  • Semiabierto: tras el enfriamiento, permite una petición de prueba; el éxito cierra el breaker, el fallo lo reabre.

Combina el breaker con una ruta de fallback y la caída se vuelve una degradación en vez de un fallo duro. Aquí es también donde un gateway se gana el sueldo: Brievio hace failover entre proveedores, así que un único proveedor que se apague puede enrutar a uno sano antes de que tu breaker necesite siquiera abrirse. Para ver cómo encaja eso en un presupuesto de fiabilidad real, lee cómo diseñamos un SLO del 99,95%.

Una nota sobre lo que cuestan los reintentos

El ajuste agresivo de reintentos pone nervioso a más de uno por la factura — cada reintento es otra llamada facturable, ¿verdad? No en un gateway que factura con honestidad. Brievio no cobra nada por las llamadas fallidas 4xx/5xx, así que el 429 que disparó tu backoff y el 503 que reintentaste de más son gratis. Pagas por el intento que de verdad devuelve una terminación, no por los que el sistema rechazó. Eso significa que puedes ajustar el número de intentos y los timeouts en pos de la fiabilidad sin penalización del contador por hacerlo — y si los reintentos desbocados siguen siendo una preocupación de presupuesto, limita el gasto directamente como se describe en cómo limitar tu gasto en API de IA.

La conclusión

El manejo de errores en producción para las API de IA son seis reglas, y puedes desplegarlas todas en una tarde:

  • Reintenta solo 429 y 5xx. Nunca reintentes otros 4xx — son tu bug, sacado a la luz.
  • Aplica backoff exponencial con jitter completo, y acota tanto los intentos como la espera.
  • Respeta Retry-After cuando el servidor lo envíe — supera a cualquier fórmula.
  • Fija timeouts explícitos por petición y un plazo global; nunca dejes que una llamada se cuelgue.
  • Haz seguros los reintentos con efectos secundarios mediante una clave de idempotencia.
  • Añade un circuit breaker para que una caída sostenida degrade en vez de fundirse.

Nada de esto es exótico — es la infraestructura aburrida que mantiene la promesa aburrida de seguir en pie. Y funciona mejor sobre una capa base que falla rápido, expone señales reales y no te cobra por los fallos, de modo que tu ajuste sea honesto y gratis. Si todavía estás eligiendo a dónde apuntar tu tráfico, el comportamiento de fiabilidad bajo fallo es una de las cosas que vale la pena probar primero — cubierto en cómo elegir un gateway de API de IA.