Source: https://aiprimetech.io/es/blog/rate-limits-and-retries/ Jun 23, 2026 · 7 min · API # Gestión de límites de solicitudes y reintentos en las API de LLM A Por [AI Prime Tech](https://aiprimetech.io/es/) · Con asistencia de IA A las 09:12 de un martes, nuestro flujo de procesamiento de resúmenes parecía funcionar bien: 96 procesos de trabajo activos, una latencia mediana del modelo de unos 2.8 segundos y ninguna tasa elevada de errores 5xx. Entonces, un cliente subió un lote acumulado de 38,000 solicitudes de soporte. En cuatro minutos, el flujo de procesamiento estaba «caído» sin que ningún servidor lo estuviera. Estábamos alcanzando dos límites distintos a la vez: - requests_per_minute: demasiadas llamadas a la API - tokens_per_minute: demasiado volumen de tokens de entrada y salida La lógica ingenua de reintentos empeoró la situación. Cada proceso de trabajo recibía 429, esperaba un segundo y reintentaba al mismo tiempo que los demás. Eso generaba una oleada de reintentos, que producía más 429, lo que generaba otra oleada. El sistema había creado accidentalmente un ataque sincronizado de denegación de servicio contra su propia cuota. Gestionar los límites de uso de los LLM no consiste solo en «añadir una espera exponencial». En producción, debes considerar los tokens, las colas, la concurrencia, los reintentos, la idempotencia, los proveedores alternativos y el comportamiento de la facturación como un único sistema. ## Los dos límites que importan: RPM y TPM La mayoría de las API de LLM aplican alguna combinación de: - RPM: solicitudes por minuto - TPM: tokens por minuto - RPD / TPD: solicitudes o tokens por día - Límites de solicitudes simultáneas: número máximo de llamadas en curso - Límites específicos de cada modelo: límites más altos para modelos más pequeños y más bajos para modelos de vanguardia Lo importante: Los límites de RPM y TPM se alcanzan de formas distintas. Si envías muchos prompts pequeños, alcanzas primero el límite de RPM. Si envías menos prompts pero más grandes, alcanzas primero el límite de TPM. Ejemplo: ``` Limit: - 600 requests/minute - 300,000 tokens/minute Workload A: - 1,000 short classification calls/minute - 200 tokens each - 200,000 tokens/minute total Result: - TPM is fine - RPM fails Workload B: - 100 long document analysis calls/minute - 5,000 tokens each - 500,000 tokens/minute total Result: - RPM is fine - TPM fails ``` Un problema frecuente: los desarrolladores limitan las solicitudes, pero no los tokens. Eso funciona en el entorno de pruebas, donde los prompts son cortos. Luego, los usuarios de producción pegan PDF, historiales de chat, registros o contratos legales, y el presupuesto de tokens se agota. En la práctica, considero que cada llamada a un LLM reserva dos recursos: ``` cost = { requests: 1, tokens: estimated_prompt_tokens + max_output_tokens } ``` Aunque el modelo devuelva menos tokens de salida que max_output_tokens, reservar el límite superior evita asignar más capacidad de la disponible. ## Qué ocurre realmente cuando ignoras el límite de TPM Supongamos que tienes: ``` TPM limit: 120,000 Average request: 4,000 input tokens + 1,000 max output tokens Reserved cost: 5,000 tokens ``` Tu rendimiento máximo real no es «tantos procesos de trabajo como sea posible». Es: ``` 120,000 / 5,000 = 24 requests per minute ``` Eso equivale, de media, a una solicitud cada 2.5 segundos. Si ejecutas 100 procesos de trabajo, es posible que todas las solicitudes del primer lote se inicien correctamente si el proveedor permite ráfagas de tráfico. Después, la cuenta alcanza un límite infranqueable. Cada solicitud posterior recibe 429, y los reintentos se acumulan. La solución no consiste solo en reducir el número de procesos de trabajo. Necesitas un planificador que tenga en cuenta las reservas de tokens. ## Un limitador de uso mínimo que tiene en cuenta los tokens Para un único proceso, un algoritmo de cubeta de tokens suele ser suficiente. La capacidad se repone con el tiempo y cada solicitud debe obtener tanto una unidad de capacidad para solicitudes como capacidad para tokens. Aquí tienes un ejemplo simplificado en Python: ``` import asyncio import time from dataclasses import dataclass @dataclass class Bucket: capacity: float refill_per_second: float tokens: float updated_at: float def refill(self): now = time.monotonic() elapsed = now - self.updated_at self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_per_second) self.updated_at = now async def acquire(self, amount: float): while True: self.refill() if self.tokens >= amount: self.tokens -= amount return missing = amount - self.tokens sleep_for = missing / self.refill_per_second await asyncio.sleep(min(sleep_for, 1.0)) class LLMRateLimiter: def __init__(self, rpm: int, tpm: int): now = time.monotonic() self.request_bucket = Bucket( capacity=rpm, refill_per_second=rpm / 60, tokens=rpm, updated_at=now, ) self.token_bucket = Bucket( capacity=tpm, refill_per_second=tpm / 60, tokens=tpm, updated_at=now, ) self.lock = asyncio.Lock() async def acquire(self, estimated_tokens: int): async with self.lock: await self.request_bucket.acquire(1) await self.token_bucket.acquire(estimated_tokens) ``` Uso: ``` estimated_tokens = prompt_tokens + max_output_tokens await limiter.acquire(estimated_tokens) response = await client.chat.completions.create( model="gpt-5.5", messages=messages, max_tokens=max_output_tokens, ) ``` Esto es deliberadamente sencillo. En un sistema distribuido, usa Redis, bloqueos consultivos de Postgres o una cola con despacho centralizado. El principio importante es el mismo: limita tanto el número de solicitudes como el presupuesto de tokens antes de llamar a la API. ## Reintentos: la espera progresiva es necesaria, pero no suficiente Los reintentos son útiles para: - Respuestas transitorias 429 respuestas - Tiempos de espera de red agotados - Errores temporales 500, 502, 503, o 504 errores - Sobrecarga del proveedor - Conexiones restablecidas Los reintentos son peligrosos para: - Errores de validación - Errores de longitud de contexto - Fallos de autenticación - Rechazos por políticas de seguridad - Prompts defectuosos que fallan de forma determinista - Llamadas a herramientas no idempotentes La política de reintentos debe clasificar primero los errores. | Tipo de error | ¿Reintentar? | Acción habitual | 429 rate_limit_exceeded | Sí | Aplicar una espera progresiva, respetar las cabeceras de reintento y reducir la concurrencia | 500/502/503/504 | Sí | Aplicar una espera progresiva con variación aleatoria y un número limitado de intentos | Tiempo de espera agotado antes de recibir el cuerpo de la respuesta | Quizá | Reintentar con una clave de idempotencia si se admite | Longitud de contexto excedida | No | Comprimir, dividir en fragmentos o enrutar a un modelo con mayor contexto | Clave de API no válida | No | Desactivar la clave y alertar al operador | Rechazo por contenido o seguridad | No | Devolver un fallo controlado o reformular la solicitud | El efecto secundario de la herramienta ya se ha ejecutado | No/Quizá | Requiere un diseño que garantice la idempotencia La forma básica de la espera progresiva: ``` import random def retry_delay(attempt: int, base=0.5, cap=30.0): exponential = min(cap, base * (2 ** attempt)) return random.uniform(0, exponential) ``` Ese random.uniform(0, exponential) es el componente de variación aleatoria. No lo omitas. Sin variación aleatoria: ``` 100 workers fail 100 workers sleep 1s 100 workers retry 100 workers fail 100 workers sleep 2s 100 workers retry ``` Con variación aleatoria: ``` 100 workers fail workers retry across a spread of 0-1s then 0-2s then 0-4s ``` El segundo patrón da margen al proveedor y a tu propia cola para recuperarse. ### Respeta las cabeceras de reintento cuando estén presentes Muchas API incluyen una indicación como: ``` Retry-After: 12 ``` o cabeceras de restablecimiento específicas del proveedor. Si la API te indica una espera concreta, úsala como mínimo: ``` delay = max(header_retry_after_seconds, retry_delay(attempt)) await asyncio.sleep(delay) ``` En la práctica, también limito el tiempo total de reintento. Una solicitud de chat de cara al usuario no debería reintentarse silenciosamente durante dos minutos mientras el usuario mira un indicador de carga. Un trabajo por lotes en segundo plano puede esperar mucho más. Ejemplo de política: ``` { "interactive_chat": { "max_attempts": 3, "max_total_retry_seconds": 8 }, "batch_summarization": { "max_attempts": 8, "max_total_retry_seconds": 300 } } ``` ## Las colas son mejores que las avalanchas de procesos de trabajo Si tienes más trabajo que cuota disponible, los procesos de trabajo no deberían llamar todos directamente a la API del LLM. Coloca una cola delante. Una arquitectura práctica: ``` App/API -> Job queue -> Rate-aware dispatcher -> Provider client -> Result store ``` El despachador se encarga de: - Contabilizar las RPM - Contabilizar los TPM - Seleccionar el modelo - la política de reintentos - el cambio a un proveedor alternativo - los límites de concurrencia - la prioridad de la cola Esto hace visible la sobrecarga. En lugar de que miles de tareas fallen con 429, ves cómo aumenta el número de tareas en la cola y el tiempo de espera estimado. En los sistemas basados en Redis, me gusta separar los trabajos «listos para ejecutarse» de los trabajos con «reintento diferido»: ``` llm:ready llm:delayed llm:dead_letter ``` Un trabajo fallido no vuelve inmediatamente a ready. Se le asigna una marca de tiempo para el reintento programado. Un pequeño planificador devuelve a la cola de trabajos listos aquellos cuyo momento de ejecución ya ha llegado. ``` { "job_id": "sum_7f13", "model": "claude-sonnet-4.6", "estimated_tokens": 6200, "attempt": 2, "not_before": "2026-02-03T09:14:22Z" } ``` Ese not_before evita las avalanchas de reintentos. ## El control de concurrencia no es lo mismo que la limitación de tasa Los límites de concurrencia restringen el trabajo en curso. Los límites de tasa restringen el trabajo por intervalo de tiempo. Necesitas ambos. Imagina que cada llamada tarda 20 segundos y tu límite de RPM es 60. Un limitador basado únicamente en RPM permite una solicitud por segundo. Después de 20 segundos, tienes unas 20 llamadas simultáneas. Si la latencia aumenta a 90 segundos, las mismas RPM generan 90 llamadas simultáneas. Eso puede superar los límites del proveedor o de los recursos locales. Usa un semáforo para controlar la concurrencia: ``` provider_concurrency = asyncio.Semaphore(20) async def call_model(job): async with provider_concurrency: await limiter.acquire(job.estimated_tokens) return await send_request(job) ``` Un error habitual: adquirir el semáforo demasiado pronto hace que los procesos de trabajo permanezcan inactivos, ocupando plazas de concurrencia escasas mientras esperan tokens de autorización de tasa. Por lo general, primero debes obtener capacidad de tasa y después adquirir una plaza de concurrencia justo antes de la llamada de red. Si la espera del limitador puede ser larga, evita ocupar recursos de los procesos de trabajo durante esa espera. ## Degradación controlada: dedica calidad donde importa Cuando los límites de tasa aprietan, tienes otras opciones además de fallar. Por ejemplo, un asistente de atención al cliente podría reducir sus prestaciones de esta forma: | Nivel de presión | Estrategia | Impacto en el usuario | Normal | Usar el modelo configurado más potente | La mejor calidad | Presión leve sobre los TPM | Reducir max_tokens, recortar el contexto recuperado | Respuestas ligeramente más cortas | Presión moderada | Dirigir las tareas sencillas a un modelo más pequeño | Mínimo si la clasificación o el enrutamiento son sencillos | Presión alta | Poner en cola los trabajos no urgentes | Trabajo en segundo plano retrasado | Presión extrema | Devolver una respuesta almacenada en caché o basada en una plantilla | Menor calidad, pero con disponibilidad Ejemplos concretos: - Dirige el etiquetado de tickets a Haiku 4.5, Qwen o una variante pequeña de Llama en lugar de usar un modelo de vanguardia. - Usa Sonnet 4.6 para el razonamiento habitual y reserva Opus 4.8 o GPT-5.5 para los casos que requieran escalamiento. - Traslada el análisis de contextos largos a Fable 5 o Gemini 3 solo cuando el prompt realmente necesite esa ventana de contexto. - Para los trabajos por lotes, pausa los inquilinos de baja prioridad antes de afectar al tráfico interactivo. La contrapartida es la complejidad. Cada ruta alternativa necesita pruebas, observabilidad y aceptación por parte del equipo de producto. En algunos ámbitos, una respuesta peor puede ser más perjudicial que una respuesta tardía. ## Alternativas con varias claves y varios proveedores El enrutamiento con varias claves y varios proveedores puede mejorar la resiliencia, pero es fácil implementarlo mal. Hay tres patrones distintos: | Patrón | Qué resuelve | Riesgo | Varias claves, mismo proveedor o cuenta | Aislamiento operativo | Puede infringir las condiciones del proveedor si se usa para eludir los límites | Varias cuentas o regiones | Aislamiento de fallos | Mayor complejidad de facturación y gobernanza | Varios proveedores o modelos | Disponibilidad y flexibilidad de costes | Variabilidad de las respuestas e incompatibilidad de los prompts No uses varias claves para eludir los límites a escondidas. Úsalas para una separación legítima: inquilinos, entornos, productos o ámbitos de facturación. Una política de enrutamiento clara podría ser así: ``` { "task": "support_ticket_summary", "primary": { "provider": "anthropic", "model": "claude-sonnet-4.6" }, "fallbacks": [ { "provider": "openai", "model": "gpt-5.5", "when": ["provider_5xx", "rate_limit_after_retry_budget"] }, { "provider": "google", "model": "gemini-3", "when": ["context_length_needed", "primary_unavailable"] } ], "degrade_to": { "provider": "open_model_cluster", "model": "qwen", "when": ["batch_low_priority"] } } ``` Si usas un agregador o una plataforma como AI Prime Tech para acceder a varios modelos, siguen aplicándose las mismas reglas de ingeniería: lleva un control explícito de la idempotencia, los presupuestos, las condiciones para recurrir a alternativas y las reglas de facturación final. ## Evitar la doble facturación al recurrir a alternativas Lo difícil de recurrir a una alternativa es saber si la primera solicitud se completó. Considera esta secuencia: ``` Client sends request to Provider A Provider A processes it successfully Network connection drops before client receives response Client retries with Provider B Both providers bill Two completions may exist ``` No puedes resolver esto por completo desde el lado del cliente a menos que el proveedor admita claves de idempotencia o la consulta de solicitudes. Pero puedes reducir los daños. ### Usa claves de idempotencia cuando estén disponibles Genera una clave estable por cada operación lógica: ``` import hashlib import json def idempotency_key(task_name, tenant_id, input_payload): canonical = json.dumps(input_payload, sort_keys=True, separators=(",", ":")) digest = hashlib.sha256(canonical.encode()).hexdigest() return f"{tenant_id}:{task_name}:{digest}" ``` Guarda la operación antes de enviarla: ``` { "operation_id": "acme:summarize:9d2a...", "status": "in_flight", "provider": "anthropic", "model": "claude-sonnet-4.6", "created_at": "2026-02-03T09:12:01Z" } ``` Después, actualízala una vez completada: ``` { "operation_id": "acme:summarize:9d2a...", "status": "completed", "provider_request_id": "req_abc123", "usage": { "input_tokens": 4180, "output_tokens": 612 } } ``` Antes de reintentar con otro proveedor, consulta la tabla de operaciones. Si otro proceso de trabajo ya la ha completado, devuelve el resultado almacenado. ### Distingue los tipos de tiempo de espera agotado No todos los tiempos de espera agotados son iguales: - Tiempo de espera de conexión agotado: probablemente la solicitud nunca llegó al proveedor - Tiempo de espera de lectura agotado antes de recibir las cabeceras: resultado incierto - Tiempo de espera agotado durante la transmisión de tokens: casi con certeza el proveedor la procesó - Cancelación del cliente: depende de si la cancelación se propagó Para los casos inciertos, prefiero una política de reintentos conservadora: ``` interactive: - retry connect timeout - do not cross-provider retry after partial stream - show recoverable error if status unknown batch: - retry after reconciliation delay - prefer same provider with idempotency key - fallback only after retry budget expires ``` ### Transmite con cuidado La transmisión en streaming mejora la latencia percibida, pero complica los reintentos. Una vez que has enviado una respuesta parcial al usuario, reintentar con otro modelo puede producir una continuación diferente. Para el chat en streaming, suelo considerar la «recepción del primer token» como el punto a partir del cual ya no se puede reintentar de forma transparente. A partir de ahí, los fallos se hacen visibles: ``` "The response was interrupted. Retry?" ``` Eso es menos elegante que una recuperación silenciosa, pero evita unir respuestas incompatibles de distintos modelos. ## Observabilidad: las métricas que realmente ayudan Necesitas algo más que la tasa de éxito. Registra al menos: ``` llm_requests_total{provider,model,status} llm_tokens_reserved_total{provider,model} llm_tokens_used_total{provider,model} llm_retries_total{provider,model,error_type} llm_queue_depth{priority,task_type} llm_queue_wait_seconds{priority,task_type} llm_rate_limit_hits_total{provider,model,limit_type} llm_fallbacks_total{from_provider,to_provider,reason} llm_duplicate_suppressed_total{task_type} ``` En los incidentes reales, el panel más útil del cuadro de mando a menudo no es el de latencia. Es: ``` queue_wait_seconds p50 / p95 rate_limit_hits by provider and model tokens_reserved vs tokens_used fallback rate by reason ``` Si tokens_reserved es sistemáticamente mucho mayor que tokens_used, tus reservas son demasiado conservadoras. Si se acerca demasiado al uso real o queda por debajo, estás asignando más capacidad de la disponible y alcanzarás el límite de TPM inesperadamente. ## Un envoltorio práctico para reintentos Aquí tienes un patrón compacto para reintentar solo los fallos que lo permiten: ``` import asyncio import random import time RETRYABLE_STATUS = {429, 500, 502, 503, 504} class LLMError(Exception): def __init__(self, status, message="", retry_after=None): self.status = status self.retry_after = retry_after super().__init__(message) def jittered_backoff(attempt, base=0.5, cap=20): return random.uniform(0, min(cap, base * (2 ** attempt))) async def call_with_retries(fn, max_attempts=4, max_elapsed=20): started = time.monotonic() for attempt in range(max_attempts): try: return await fn() except LLMError as error: elapsed = time.monotonic() - started if error.status not in RETRYABLE_STATUS: raise if attempt == max_attempts - 1 or elapsed >= max_elapsed: raise delay = jittered_backoff(attempt) if error.retry_after is not None: delay = max(delay, error.retry_after) remaining = max_elapsed - elapsed await asyncio.sleep(min(delay, remaining)) ``` El envoltorio es deliberadamente sencillo. La sofisticación debe estar en la clasificación, las colas, la idempotencia y el enrutamiento, no en un bucle mágico de reintentos que lo intente indefinidamente. ## Conclusiones prácticas - Modela los límites de tasa tanto en RPM como en TPM. La regulación del tráfico basada en tokens evita sorpresas con los prompts grandes. - Usa retroceso exponencial con variación aleatoria, y respeta las cabeceras de reintento cuando el proveedor las proporcione. - Coloca una cola y un despachador entre tu aplicación y los proveedores de LLM para cargas de trabajo por lotes o de gran volumen. - Control la concurrencia por separado de los límites de tasa; de lo contrario, los picos de latencia pueden generar demasiadas llamadas en curso. - Diseño degradación controlada de forma deliberada: modelos más pequeños, un contexto más corto, trabajo en segundo plano en cola o respuestas en caché. - Trata conmutación a proveedores alternativos como una función de fiabilidad, no como una licencia para enviar solicitudes duplicadas indiscriminadamente. - Usa claves de idempotencia y registros de operaciones para reducir la doble facturación y suprimir las respuestas generadas duplicadas. - Sé transparente con los usuarios cuando una respuesta en streaming falle después de haber emitido tokens. - Crea paneles centrados en la espera en cola, la reserva de tokens, los eventos de límite de tasa, los reintentos y los motivos de conmutación a alternativas. - Mantén sencilla la lógica de reintentos; el verdadero trabajo en producción consiste en asignar presupuestos, programar la ejecución y saber cuándo no reintentar. [API](/es/blog/tag/api/) A [AI Prime Tech](https://aiprimetech.io/es/) AI Prime Tech publica este blog. Los artículos se redactan con asistencia de IA; comprueba los detalles específicos de cada versión, como los identificadores de modelos, los precios y los límites, en la documentación oficial antes de basarte en ellos. Obtén acceso más económico a la API de Claude Una sola clave de API para Claude Opus 5.5, Sonnet 5, Haiku 4.5 y Fable 5.1, además de GPT-6 modelos. Pago por uso, sin suscripción. [Obtén tu clave de API →](/register) AI Prime Tech es una pasarela de API independiente de terceros. Claude™ y Anthropic® son marcas comerciales de Anthropic, PBC. No se implica ninguna afiliación ni respaldo.