Manejo del límite 429: cola, backoff y estados claros para el usuario
Manejo del límite 429 para apps LLM y API: encola solicitudes, aplica backoff seguro y muestra estados claros al usuario para que el producto sea predecible.

Qué significa realmente un 429 (y por qué los usuarios lo notan)
Un 429 es el proveedor diciendo: baja el ritmo. Estás enviando demasiadas solicitudes muy rápido. Normalmente no significa que tu código esté "mal." Significa que alcanzaste un límite establecido para proteger sus sistemas (y a veces tu propio presupuesto).
Los usuarios notan el mal manejo de 429 incluso cuando la app funciona en su mayor parte. La experiencia se vuelve desordenada: un spinner que nunca termina, un error tras una larga espera, o respuestas que a veces aparecen y otras veces fallan. Lo peor es la inconsistencia. La gente no puede prever qué pasará.
Los reintentos pueden ayudar, pero los reintentos descuidados suelen empeorar las cosas. Si cada solicitud fallida reintenta al instante, creas un pico de tráfico justo cuando el proveedor te está pidiendo reducir la carga. Eso puede convertir un pequeño tropiezo en una interrupción mayor, y consumir cuotas rápidamente.
El objetivo no es la máxima velocidad. Es un comportamiento predecible bajo carga. La app debería comportarse igual cada vez, incluso si la respuesta llega más tarde.
Un modelo mental simple son tres piezas que trabajan juntas:
- Una cola para suavizar ráfagas en lugar de dispararlo todo a la vez
- Backoff (a menudo exponencial con jitter) para espaciar los reintentos
- Estados claros para el usuario, para que la gente sepa si la petición está en espera, reintentando o requiere acción
Ejemplo: una app de chat recibe un pico matutino. Sin control, la mitad de los mensajes fallan al azar. Con una cola y backoff, los mensajes pueden tardar más, pero los usuarios ven "En cola" y luego "Reintentando en 4s." La app se siente calma y fiable en vez de rota.
De dónde suelen venir los límites en apps reales
La mayoría de los 429s no son fallos aleatorios. Son una señal de que demasiadas solicitudes golpearon un límite al mismo tiempo y tu app no distribuyó la carga.
El desencadenante más común es una ráfaga: un pico de tráfico tras un lanzamiento, una publicación en redes, o un cliente grande que invita a su equipo. Los jobs por lotes también lo hacen, como reindexar contenido, importar CSVs o rellenar embeddings durante la noche.
También importa a qué límite llegaste. Los proveedores suelen imponer topes por clave API, por cuenta o por región. Tu propia app puede crear límites también, intencionalmente o por accidente, como por usuario, por workspace o por IP (especialmente si muchos usuarios están tras un NAT corporativo).
Las apps LLM tienen algunos patrones que alcanzan límites más rápido de lo que esperas:
- Las respuestas por streaming mantienen conexiones abiertas más tiempo, así que la concurrencia sube aunque la QPS parezca normal.
- Las llamadas a herramientas multiplican trabajo, porque una "respuesta" puede desencadenar llamadas extra para obtener datos, ejecutar funciones y volver a preguntar al modelo.
- Los embeddings en bloque son clásicos: una sola acción de "importar documentos" puede lanzar cientos de solicitudes en un bucle cerrado.
Los multiplicadores ocultos son el culpable habitual. Un clic puede crear muchas llamadas descendentes:
- Enviar un chat desencadena moderación, la completación principal y una llamada de logging
- Un solo mensaje de usuario puede generar 3–10 llamadas a herramientas antes de la respuesta final
- Un botón de "reintentar" dispara al instante y añade más presión
- Cargar una página lanza múltiples peticiones paralelas en varias pestañas
- Un worker en segundo plano repite el mismo job fallido en muchas cuentas
Staging suele verse bien porque tiene un usuario, datos limpios y sin cron jobs. Producción tiene concurrencia, picos reales, sesiones de streaming de larga duración y tormentas accidentales como varios workers iniciando la misma backfill.
Elige una política clara: reintentar, retrasar o detener
Cuando recibes un 429, lo peor es "intentar de nuevo inmediatamente" sin reglas. Eso hace que el proveedor te empuje más, y tu app parezca aleatoria. Un buen manejo de 429 comienza con una política simple que todo el equipo pueda seguir.
Divide las solicitudes en dos cubos:
- Acciones interactivas (un usuario hizo clic)
- Trabajo en segundo plano (syncs, webhooks, jobs por lotes)
Las acciones interactivas necesitan una ventana de paciencia corta y retroalimentación clara. El trabajo en background puede esperar más, siempre que no se acumule para siempre.
Una política práctica que puedes adoptar y ajustar después:
- Reintentar solo cuando la solicitud sea segura de repetir (llamadas de solo lectura, o escrituras con idempotencia).
- Establecer una ventana máxima de reintentos (por ejemplo, 10–20 segundos para interactivas, 5–30 minutos para background).
- Detenerse y fallar rápido cuando el usuario puede solucionarlo cambiando su comportamiento (como spamear "Regenerate") o cuando falta input requerido.
- Usar prioridades separadas: las interactivas van primero, el trabajo en background cede cuando los límites aprietan.
- Registrar contexto en cada 429 para poder depurar patrones, no adivinar.
La idempotencia es lo que hace seguros los reintentos. Si una operación crea algo (cobrar una tarjeta, crear un registro, enviar un email), añade una clave de idempotencia para que un reintento no duplique el efecto. Si no puedes hacerlo idempotente, prefiere "detener y preguntar" en lugar de reintentos ciegos.
Define "hecho" para cada petición. Una petición debe o bien tener éxito, devolver un error claro, o expirar tras tu ventana máxima de reintentos. Quedar colgando para siempre es lo más frustrante.
Registra lo suficiente para responder lo básico: qué endpoint fue llamado, qué usuario lo disparó, qué tipo de solicitud era (interactiva vs background), cuándo empezó y cuánto esperó.
Paso a paso: añade una cola de peticiones que suavice las ráfagas
La forma más rápida de mejorar el manejo de 429 es dejar de disparar solicitudes en el mismo instante en que la IU las solicita. En su lugar, pon cada llamada API en una cola. Eso convierte una ráfaga de 50 clics en un flujo controlado que el proveedor realmente puede aceptar.
1) Empieza con una cola pequeña entre tu UI y el proveedor
Mantén la primera versión aburrida. Cuando la app quiera llamar al LLM o a una API, crea un job con el payload y unas pocas etiquetas (quién lo pidió, para qué pantalla es, cuándo se creó). La UI envía jobs a la cola, no directamente al proveedor.
Una forma práctica de job incluye: endpoint/modelo, prompt o hash de parámetros, ID de usuario, prioridad y contador de reintentos.
2) Añade un worker con límite de concurrencia
Un worker saca jobs de la cola y los ejecuta. La configuración clave es la concurrencia: cuántos jobs pueden correr al mismo tiempo. Empieza bajo (como 1 a 3) y sube solo si ves resultados estables.
Un enfoque simple que funciona en la mayoría de apps:
- Procesa solo N jobs en paralelo (tu límite de concurrencia)
- Cuando un job termina, inicia inmediatamente el siguiente
- Si un job recibe un 429, vuelve a ponerlo en la cola con un retraso (el backoff se cubre luego)
- Rastrea jobs activos para nunca exceder N
3) Prioridades: mantén la app reactiva
No todas las solicitudes son iguales. Un botón que el usuario acaba de pulsar debe adelantarse al trabajo en background como "resumir todo" o "generar informe semanal." Usa dos prioridades (alta y baja) al principio. Si tu cola lo permite, trata la alta como un carril separado.
4) Desduplicar solicitudes repetidas
Los usuarios hacen doble clic. Los frontends re-renderizan. Si envías prompts idénticos dentro de una ventana corta (digamos 2 a 10 segundos), pásalos por un merge. Devuelve el mismo resultado en vuelo a cada llamador. Esto por sí solo puede reducir mucho el volumen de solicitudes.
5) Persiste el estado de la cola para que reinicios no pierdan trabajo
Si el servidor se reinicia, no quieres que los jobs pendientes desaparezcan o se repitan de forma impredecible. Almacena los jobs en cola y su estado en almacenamiento duradero (una BD o un sistema de colas adecuado). Aquí es donde fallan muchos prototipos: las solicitudes son fire-and-forget y los usuarios ven respuestas faltantes, duplicados o spinners eternos cuando los límites aparecen.
Paso a paso: backoff que reduce la presión en vez de aumentarla
El buen manejo de 429 se trata sobre todo de hacer menos, no de esforzarse más. Si reintentas demasiado rápido, conviertes una pequeña lentitud en un pico de tráfico y mantienes a los usuarios atrapados en un bucle.
Empieza con backoff exponencial: tras el primer 429, espera un poco; tras el siguiente, espera más. Una regla simple es doblar el retraso cada vez. Esto da tiempo al proveedor para recuperarse y evita que tu app martillee la API.
Añade jitter (una pequeña aleatoriedad) a cada espera. Sin jitter, miles de clientes que alcanzaron el límite al mismo tiempo vuelven a reintentar al mismo tiempo y vuelven a alcanzar el límite. El jitter reparte los reintentos para que tu app tenga una oportunidad de éxito.
Si el proveedor te da una pista como un valor Retry-After, trátalo como la fuente de la verdad. Úsalo como retraso base y aplica un poco de jitter alrededor.
Establece techos claros para que los reintentos no duren para siempre:
- Intentos máximos (por ejemplo 3–6 intentos)
- Retraso máximo (por ejemplo cap en 30–60 segundos)
- Presupuesto total de tiempo (deja de intentar, por ejemplo, tras 2 minutos)
- Ruta de fallback (guardar la tarea, pedir al usuario intentarlo más tarde)
Deja de reintentar en errores que no se arreglarán solos. Un 400 fallará siempre. Un 401/403 significa que la autenticación está rota. Reintenta solo cuando sea probable que tenga éxito (429, muchos 5xx y algunos timeouts de red).
Ejemplo: una función de chat recibe 429 durante un lanzamiento. Intento 1 espera 1–2 segundos, intento 2 espera 2–4 segundos, intento 3 espera 4–8 segundos, luego te rindes y muestras un mensaje claro de que el sistema está ocupado y el mensaje se enviará cuando sea posible.
Estados visibles para el usuario que mantienen la app predecible
Cuando recibes un límite, lo peor no es la demora. Es la confusión. Si la UI parece congelada o sigue alternando entre errores y spinners, los usuarios volverán a hacer clic, refrescarán o abrirán otra pestaña. Eso genera más solicitudes y alarga el problema.
Un buen manejo de 429 comienza con nombrar lo que ocurre en lenguaje llano y dar un paso siguiente claro.
Usa un pequeño conjunto de estados claros
Mantén los estados consistentes en toda la app. La mayoría de equipos solo necesitan tres:
- En cola: "Estamos en línea para enviar tu solicitud."
- Reintentando: "El proveedor está ocupado. Intentando de nuevo en 10–20 segundos."
- Intentar de nuevo: "No pudimos enviar esto aún. Intenta de nuevo ahora."
Añade una expectativa de tiempo aproximada cuando puedas, aunque sea solo un rango. Puedes basarlo en la posición en la cola (por ejemplo, 3 solicitudes por delante) o en tu temporizador de backoff. Una pequeña cuenta regresiva ayuda a que la gente espere sin hacer clic.
Ofrece un botón seguro de Cancelar para esperas largas. Explica qué significa cancelar en una frase: "Cancelar detiene los reintentos. Tu borrador se queda aquí y nada se envía." Si cancelar haría perder trabajo, dilo claramente y ofrece "Guardar borrador" en su lugar.
Evita errores crudos como "429" o "Too Many Requests." Tradúcelo a lo que le importa al usuario: "El proveedor de IA nos pide bajar el ritmo." Luego ofrece una acción: seguir esperando (por defecto) o intentar de nuevo.
Para esperas mayores a unos pocos segundos, muestra una notificación ligera cuando esté listo, como un banner en la app que diga "Tu respuesta está lista" o un cambio de estado en el mensaje. Esto es especialmente importante en chat.
Protege el resto de tu app mientras el proveedor te frena
Cuando un proveedor empieza a devolver 429s, el mayor riesgo no es la llamada fallida. Es el efecto dominó: hilos bloqueados, colas creciendo sin límite y partes no relacionadas de la app sintiéndose lentas. Un buen manejo de 429 se trata sobre todo de contención.
Empieza con timeouts en todas partes donde una llamada al proveedor pueda colgarse. Un reintento que espera para siempre no es paciente; es un bloqueador. Ten un tiempo máximo por intento y un tiempo total claro a través de reintentos, para que una petición no pueda secuestrar el resto del sistema.
Un circuit breaker ayuda cuando los 429s se disparan. En lugar de dejar que cada petición siga golpeando al proveedor, pausa llamadas por una ventana corta y luego prueba con una petición pequeña. Esto hace la desaceleración predecible y evita que sigas añadiendo presión.
Separa la capacidad de respuesta de la UI de la velocidad del proveedor. La UI debe reaccionar al instante: aceptar la acción del usuario, mostrar un estado claro y procesar el trabajo en background. Si atas los clics de botones al tiempo de respuesta del proveedor, toda la app parecerá rota aun cuando solo una dependencia esté lenta.
Los presupuestos por usuario evitan que un usuario pesado (o un bug) se coma a todos los demás. Manténlo simple: cuántos jobs en cola y cuánto tiempo de reintento puede consumir un usuario antes de que empieces a retrasar o rechazar.
Señales útiles para vigilar:
- Tasa de 429 en el tiempo (picos vs contínuo)
- Profundidad de la cola (cuántos jobs esperan)
- Tiempo medio de espera antes de que un job empiece
- Tasa de timeouts (demasiado estrictos vs demasiado laxos)
- Tiempo abierto del circuit breaker (con qué frecuencia pausas llamadas)
Errores comunes que empeoran los problemas de 429
La mayor parte del dolor por 429 viene de unos pocos errores previsibles. Evítalos y el manejo de 429 se vuelve aburrido (en el buen sentido).
Reintentos que crean una tormenta de reintentos
Reintentos instantáneos o con el mismo retraso fijo pueden convertir una pequeña lentitud en una caída. Si 200 usuarios pulsan "Try again" y cada cliente reintenta en el mismo segundo, obtienes una ola sincronizada de tráfico que sigue provocando 429s.
Usa un retraso que crezca con el tiempo, más jitter, para que los reintentos se repartan.
Reintentos infinitos y costes ocultos
Sin un conteo máximo de intentos y un tiempo máximo total, una sola acción de usuario puede disparar un bucle sin límites. Eso puede quemar tokens o créditos API, mantener workers ocupados en trabajo inútil y crear pantallas de "cargando para siempre".
Limita los reintentos y detente con un mensaje claro cuando alcanzas el límite.
Un límite global sin prioridades
Si tratas todas las llamadas igual, el trabajo en background (como embeddings) puede dejar sin recursos flujos críticos como login, checkout o "Enviar mensaje." Usa colas o prioridades separadas para que las acciones clave ganen siempre.
Ignorar el éxito parcial
Un caso común: una llamada tiene éxito (creaste un mensaje), pero la siguiente recibe 429 (fallaste al obtener el historial actualizado). Si tratas toda la operación como fallida, arriesgas duplicados, actualizaciones UI faltantes o estado roto.
Trata cada llamada como un paso independiente. Guarda lo que tuvo éxito, reintenta solo lo que falló.
Errores genéricos que enseñan a los usuarios a hacer más clics
"Algo salió mal" hace que la gente martillee el botón, lo que genera más carga. Muestra un estado específico como: "Estamos esperando al proveedor. Reintentando en 8 segundos."
Lista rápida antes de lanzar
Antes del lanzamiento, trata el manejo de 429 como una característica de producto, no solo un bucle de reintentos. Una buena configuración controla costes, evita interrupciones sorpresa y hace que la app se sienta tranquila incluso cuando el proveedor dice "baja el ritmo."
Un chequeo práctico pre-lanzamiento:
- La concurrencia está limitada en dos lugares: por feature (por ejemplo, chat) y por usuario (para que una persona ocupada no deje a todos sin recursos).
- Los reintentos tienen límites claros: máximo de intentos y tiempo total gastado en reintentos (para que las solicitudes no queden colgadas para siempre).
- El backoff reduce la presión: añades jitter, respetas pistas del servidor como
Retry-Aftery evitas reintentos sincronizados entre muchos usuarios. - La UI sigue siendo predecible: los usuarios ven qué pasa ("En espera para reintentar"), cuánto puede tardar y una acción segura (Cancelar, Intentar de nuevo o Continuar sin este resultado).
- Los logs son suficientes para depurar rápido: incluye nombre del proveedor, endpoint/modelo, request ID, user ID (o token anónimo), número de intento, tiempo de espera elegido y el payload exacto del error.
Haz una prueba rápida de tormenta antes de lanzar. Abre la app en dos navegadores, dispara 10–20 acciones rápido y confirma que ves colas ordenadas, retrasos crecientes y una parada limpia cuando se alcanzan los límites.
Ejemplo: una función de chat que recibe un pico repentino
Imagina una demo de equipo: alguien comparte un enlace y en un minuto 50 personas abren el chat y envían el mismo prompt. Tu app manda 50 llamadas casi idénticas a un proveedor LLM. Unos segundos después empiezas a recibir 429s.
Sin controles, todo se siente aleatorio. Algunas solicitudes agotan el tiempo, otras reintentan al instante y vuelven a bloquearse, y los usuarios empiezan a pulsar Send una y otra vez. Ahora tienes solicitudes duplicadas, mayor coste y salidas desajustadas (una persona ve una respuesta, otra ve un error, una tercera ve dos respuestas).
Una cola cambia la forma del tráfico. En lugar de picos, alineas las solicitudes y las liberas a un ritmo estable. Añade prioridad para que la app siga siendo reactiva: acciones críticas de UI como cancelar un mensaje, cargar historial o obtener el perfil de usuario van primero, mientras las generaciones esperan su turno.
El backoff evita que empeores la situación cuando el proveedor ya está presionándote. Cuando recibes un 429, reintentas tras un retraso que crece (backoff exponencial) más una pequeña aleatoriedad (jitter). Eso impide que los 50 clientes reintenten todos exactamente al mismo momento y choquen de nuevo.
Lo que ven los usuarios importa tanto como el código. Durante el pico, una UI predecible podría mostrar:
- "En cola" con una espera estimada como 20–40 segundos
- Un botón Cancelar claro que realmente detiene el job en cola
- Una opción Reintentar solo tras un fallo, no durante la espera normal
- Un único mensaje por prompt (sin duplicados), actualizando de En cola a Enviando a Hecho
Siguientes pasos: implementa lo básico y hazlo fiable
Empieza con una página que describa tus reglas: cuándo reintentarlo, cuánto esperar, cuándo detenerse y qué ve el usuario en cada paso. Esto evita el desorden común donde el backend reintenta para siempre mientras la UI parece congelada.
Si solo haces dos tareas de ingeniería esta semana, hazlas en el lugar más pequeño que detenga el dolor primero: el cliente compartido que habla con tu proveedor. Centralizar el manejo de 429 beneficia a todas las features y evita terminar con cinco comportamientos de reintento distintos por toda la app.
Un orden práctico que suele funcionar:
- Define una política de reintentos (intentos máximos, espera máxima y qué errores son reintentables)
- Añade una cola de peticiones para suavizar ráfagas de clics y jobs en background
- Añade backoff exponencial con jitter para que los reintentos reduzcan la presión en lugar de aumentarla
- Añade estados de usuario claros: en espera, reintentando y un fallback seguro cuando te rindes
- Registra cada 429 con el retraso elegido para que puedas ver patrones luego
Luego ejecuta una prueba de carga simple. Simula 20–50 acciones rápidas (enviar mensaje, regenerar, actualizar datos) y fuerza algunos 429. La meta no es velocidad perfecta. Es que la UI siga entendible: los usuarios deben ver que la app está esperando, no rota, y deben saber qué pasará a continuación.
Si estás heredando una base de código generada por IA, ten cuidado con añadir parches encima. Ahí es donde aparecen tormentas de reintentos y solicitudes fantasma que siguen ejecutándose después de que el usuario navegue fuera.
Si quieres una segunda opinión, FixMyMess (fixmymess.ai) se centra en diagnosticar y reparar apps generadas por IA que se rompen bajo tráfico real, incluyendo reintentos descontrolados, topes faltantes y fan-out de peticiones desordenado. Una auditoría rápida puede ayudarte a llegar a un comportamiento predecible rápido.
Preguntas Frecuentes
¿Qué significa realmente un error 429?
Un 429 significa que el proveedor te está limitando porque llegaron demasiadas solicitudes en poco tiempo. Tu código puede estar bien, pero tu patrón de tráfico (picos, alta concurrencia o llamadas extra ocultas) está superando una cuota o límite de rendimiento.
¿Qué debe hacer mi app cuando recibe un 429?
Por defecto, espera y reintenta con un retraso, no reintentes al instante. Usa una ventana de tiempo corta para acciones disparadas por el usuario y una más larga para trabajos en segundo plano. Deja de reintentar cuando se agote tu presupuesto de tiempo para que la app no quede girando eternamente.
¿Por qué se recomiendan el backoff exponencial y el jitter?
El backoff exponencial hace que cada reintento espere más que el anterior, lo que reduce la presión cuando el proveedor ya está sobrecargado. El jitter añade una pequeña aleatoriedad para que muchos clientes no reintenten todos al mismo tiempo y provoquen otra ola de 429s.
¿Debo seguir la cabecera Retry-After?
Trata Retry-After como la instrucción principal sobre cuánto esperar. Si añades jitter, mantenlo pequeño alrededor de ese valor para respetar la temporización del proveedor a la vez que evitas reintentos sincronizados.
¿Realmente necesito una cola de peticiones, o puedo simplemente reintentar?
Una cola suaviza los picos convirtiendo "50 solicitudes ahora" en un flujo controlado que el proveedor puede aceptar. También te da un lugar para priorizar, retrasar y persistir tareas para que no desaparezcan ni se dupliquen en reinicios.
¿Cómo elijo un límite de concurrencia seguro?
Empieza con un límite de concurrencia bajo (a menudo 1–3 por worker o por función) y sube solo cuando veas resultados estables en tráfico parecido a producción. El número correcto es el valor más alto que no dispare 429s frecuentes durante los picos normales.
¿Cuándo son peligrosos los reintentos y cómo hacerlos seguros?
Reintenta solo solicitudes seguras de repetir, como llamadas de solo lectura o escrituras protegidas por claves de idempotencia. Si un reintento podría crear duplicados (cobros, correos, registros), implementa idempotencia primero o falla rápido y pide confirmación al usuario.
¿Debo desduplicar prompts o llamadas API idénticas?
Sí. Si ves solicitudes idénticas repetidas en un corto periodo, devuelve el mismo resultado en vuelo en lugar de iniciar llamadas nuevas. Esto reduce el tráfico por doble clic accidental y el ruido por re-renderizaciones, que a menudo causa 429s en picos.
¿Qué deben ver los usuarios mientras una petición está en espera o reintentando?
Usa estados claros y consistentes: “En cola”, “Reintentando en X segundos”, y un “Intentar de nuevo” claro cuando te rindes. Lo crucial es que la IU reconozca la acción de inmediato y explique lo que ocurre para que los usuarios no sigan haciendo clic y añadiendo carga.
¿Qué debo registrar y medir para solucionar los 429s de forma definitiva?
Registra suficiente contexto para ver patrones: qué endpoint/modelo, qué usuario o workspace, interactiva vs background, número de intento, retraso elegido y tiempo total gastado. Si heredas una base de código generada por IA con reintentos descontrolados, llamadas en fan-out o topes ausentes, FixMyMess puede auditarla y arreglar rápido los modos de fallo para que el comportamiento sea predecible bajo tráfico real.