El mecanismo de throttling de las API de AWS - El algoritmo Token Bucket y la verdad detrás del error 429
Explicamos cómo el rate limiting de las API de AWS se implementa con el algoritmo Token Bucket, el concepto de burst capacity, las diferencias en los límites según el servicio, y las medidas prácticas para evitar el throttling.
¿Por qué AWS aplica rate limiting a todas las API?
Todas las API de AWS tienen configurado un rate limit (throttling) por cuenta y por región. Las solicitudes que exceden el límite reciben un error HTTP 429 (Too Many Requests) o 503 (Service Unavailable). El rate limiting tiene dos propósitos. Primero, garantizar la equidad en un entorno multi-tenant. Si una cuenta realiza llamadas masivas a la API, afecta el rendimiento de otras cuentas que comparten la misma infraestructura. El rate limiting es un guardrail que previene el "problema del vecino ruidoso". Segundo, la protección del propio cliente. Hay casos donde un bug en la aplicación causa un bucle infinito que llama a la API decenas de miles de veces por segundo. Sin rate limiting, esta ejecución descontrolada resultaría en facturas elevadas. El rate limiting funciona como una red de seguridad que detecta tempranamente ejecuciones no intencionadas. Los valores de rate limit de cada servicio se pueden verificar en Service Quotas, y muchos límites pueden elevarse mediante solicitudes de aumento de cuota.
Cómo funciona el algoritmo Token Bucket
El throttling de las API de AWS se implementa con el algoritmo Token Bucket. Este algoritmo funciona con un bucket (contenedor) donde se reponen tokens (permisos) a una velocidad constante, y cada solicitud de API consume un token. Cuando el bucket está vacío, las solicitudes son rechazadas. Expliquemos con un ejemplo concreto. Supongamos que la API DescribeInstances de EC2 tiene un rate limit de 100 solicitudes por segundo y un burst capacity de 200. Se reponen 100 tokens por segundo en el bucket, y la capacidad máxima del bucket es de 200 tokens. En condiciones normales, el bucket está lleno (200 tokens), por lo que se pueden enviar instantáneamente 200 solicitudes (burst). Después de eso, solo se pueden procesar 100 solicitudes por segundo. El burst capacity es un buffer que absorbe picos de corta duración. En patrones donde se llaman APIs masivamente al iniciar una aplicación, el burst capacity se vuelve importante. Después de agotar el burst, se limita a la tasa constante (100 solicitudes por segundo).
Granularidad del throttling que varía según el servicio
La granularidad del throttling varía significativamente según el servicio. Las API de EC2 tienen rate limits individuales configurados por acción de API. DescribeInstances y RunInstances se gestionan en buckets separados, por lo que el throttling de DescribeInstances no afecta a RunInstances. Por otro lado, el throttling de DynamoDB se aplica a nivel de tabla. Las solicitudes que exceden la capacidad provisionada de la tabla (RCU/WCU) son throttled. Esto es diferente del throttling a nivel de API; es una limitación de throughput de acceso a datos. El límite de ejecución concurrente de Lambda también es un tipo de throttling. Las invocaciones de funciones que exceden el límite predeterminado de 1,000 ejecuciones concurrentes por cuenta son throttled con error 429. API Gateway tiene un rate limit a nivel de cuenta de 10,000 solicitudes por segundo (predeterminado), y además permite configuraciones de throttling individuales por API, por stage y por método. Este throttling multicapa permite controlar que el acceso concentrado a un endpoint específico no afecte a otros endpoints.
Backoff exponencial y jitter - La estrategia de reintentos que el SDK realiza automáticamente
La respuesta correcta al recibir un error de throttling (429) es un reintento combinando backoff exponencial (Exponential Backoff) y jitter. El backoff exponencial es una estrategia que aumenta el intervalo de reintento exponencialmente: 1 segundo, 2 segundos, 4 segundos, 8 segundos. Esto alivia gradualmente la presión de solicitudes sobre el servicio que está siendo throttled. El jitter es una técnica que añade variación aleatoria al intervalo de reintento. Solo con backoff exponencial, múltiples clientes throttled simultáneamente reintentarían al mismo tiempo, causando throttling nuevamente - el problema del "thundering herd". Al añadir jitter, los tiempos de reintento se distribuyen. Los SDK de AWS implementan automáticamente esta estrategia de reintentos internamente. El SDK v3 (JavaScript) tiene un máximo predeterminado de 3 reintentos con backoff exponencial y full jitter aplicados. boto3 (Python) también tiene una estrategia de reintentos similar. Si se llaman las API directamente sin usar el SDK, es necesario implementar esta lógica de reintentos por cuenta propia.
Patrones de diseño para evitar el throttling preventivamente
Es más ideal diseñar para no generar throttling en primer lugar que reintentar después de que ocurra. El primer patrón es la reducción de llamadas a API. En lugar de llamar a DescribeInstances de EC2 cada segundo para monitorear el estado de las instancias, usando eventos de EventBridge (EC2 Instance State-change Notification) se reciben notificaciones solo cuando hay cambios de estado. La transición de polling a event-driven reduce drásticamente el número de llamadas a API. El segundo patrón es el uso de caché. La información que no cambia frecuentemente (configuración de cuenta, lista de regiones, etc.) se cachea localmente para reducir las llamadas a API. El tercer patrón es el uso de API por lotes. BatchGetItem de DynamoDB puede obtener hasta 100 items en una sola llamada a API. Comparado con llamar GetItem 100 veces individualmente, reduce las llamadas a API en un 99%. ListObjectsV2 de S3 también puede obtener hasta 1,000 objetos en una sola solicitud con el parámetro MaxKeys. Para aprender sistemáticamente sobre diseño de API y medidas contra el throttling, los libros especializados (Amazon) son una referencia útil.