¿API con fallas? Cómo aplicar rate limiting con Redis
Aprende a proteger tu API contra picos de tráfico y ataques de denegación de servicio implementando el algoritmo Token Bucket en Redis con código práctico.
Resumen
- El algoritmo Token Bucket gestiona cuotas de peticiones reponiendo fichas en un cubo virtual de forma continua.
- Redis proporciona alta velocidad y atomicidad para validar límites de acceso distribuidos en milisegundos.
- Los scripts en Lua ejecutados en el servidor Redis evitan condiciones de carrera en entornos con múltiples nodos.
- Las respuestas HTTP 429 con cabeceras informativas guían a los clientes legítimos sobre cuándo reintentar.
- Estrategias adicionales de resiliencia, como el uso de patrones arquitectónicos paralelos, ayudan a blindar sistemas bajo carga.
El peligro invisible del tráfico descontrolado en APIs
Cuando una aplicación web crece, uno de los primeros problemas operativos que aparecen es la degradación del rendimiento causada por solicitudes excesivas. Ya sea un cliente malintencionado intentando derribar el sistema con un ataque de denegación de servicio, o un script mal escrito de un socio consumiendo recursos en bucle, el resultado suele ser el mismo: el servidor se bloquea, la base de datos agota las conexiones y los usuarios legítimos se quedan sin atención.
En la práctica, esto significa que dejar los endpoints críticos completamente abiertos es una invitación a fallos catastróficos de infraestructura. Para blindar el ecosistema, los ingenieros recurren al concepto de rate limiting, que consiste en imponer límites estrictos de velocidad y volumen de llamadas que un usuario, IP o clave de API determinada puede realizar en un intervalo de tiempo.
Por qué Token Bucket es el algoritmo ideal
Existen varias formas de controlar el flujo de datos, como el conteo simple en ventanas de tiempo fijas. Sin embargo, este modelo simplista sufre del efecto de borde, donde un usuario puede agotar el límite en el último segundo de un minuto y repetir la dosis justo en el primer segundo del minuto siguiente, generando un pico doble que sobrecarga el servidor por igual.
La solución más elegante y eficiente para este dilema es el algoritmo Token Bucket, o cubo de fichas. Imagina un cubo que almacena fichas hasta un límite máximo y las reabastece a una tasa constante por segundo. Cada solicitud consumida retira una ficha del cubo; si el cubo está vacío, la nueva solicitud es rechazada sumariamente, asegurando flexibilidad para absorber pequeños picos legítimos sin sofocar el sistema.
Implementar este tipo de control directamente en la memoria RAM de la aplicación puede parecer tentador, pero falla miserablemente en arquitecturas modernas basadas en múltiples servidores en la nube. Si tu API se ejecuta en tres contenedores diferentes detrás de un balanceador de carga, mantener el control de solicitudes aislado en cada máquina significaría que el límite real se triplicaría.
La elección de Redis para alto rendimiento
La respuesta natural a este desafío de sincronización es utilizar una base de datos en memoria extremadamente rápida como Redis. Como centraliza el almacenamiento de los datos de acceso, todas las instancias de tu API consultan y actualizan el mismo cubo de fichas en tiempo real, manteniendo la regla de negocio rigurosamente coherente sin importar dónde aterrice la solicitud.
Para garantizar que dos solicitudes simultáneas no lean y escriban en el mismo cubo al mismo tiempo generando inconsistencias, la mejor práctica en Redis es utilizar scripts en el lenguaje Lua. El servidor Redis ejecuta estos scripts de forma totalmente atómica, lo que significa que ninguna operación interfiere en medio del proceso de lectura y actualización del estado.
Implementando Token Bucket con Lua y Redis
El código a continuación demuestra un script robusto que calcula el paso del tiempo, reabastece fichas proporcionalmente y deduce la unidad de la nueva solicitud en una sola transacción rápida:
local key = KEYS[1]local max_tokens = tonumber(ARGV[1])local refill_rate = tonumber(ARGV[2])local now = tonumber(ARGV[3])local requested = tonumber(ARGV[4])local bucket = redis.call('HMGET', key, 'tokens', 'last_updated')local tokens = tonumber(bucket[1])local last_updated = tonumber(bucket[2])if not tokens then tokens = max_tokenstoken_last_updated = nowelse local delta = math.max(0, now - last_updated) tokens = math.min(max_tokens, tokens + (delta * refill_rate))endlocal allowed = 0if tokens >= requested then tokens = tokens - requested allowed = 1endredis.call('HMSET', key, 'tokens', tokens, 'last_updated', now)redis.call('EXPIRE', key, math.ceil(max_tokens / refill_rate))return {allowed, tokens}Este script elimina los cuellos de botella de concurrencia y garantiza que el conteo sea matemáticamente preciso incluso bajo miles de accesos simultáneos por segundo.
Integrando la verificación en tu API
Con el script ejecutándose en el servidor Redis, el trabajo del microservicio o API Gateway se reduce a enviar los parámetros correctos y a interpretar la respuesta obtenida. Si el resultado indica que la solicitud no fue permitida, la aplicación debe interrumpir el flujo inmediatamente y devolver un código de estado HTTP adecuado, tradicionalmente 429 Too Many Requests.
Además de devolver el código de error, es una excelente cortesía operativa incluir cabeceras informativas en la respuesta, como 'Retry-After', informando exactamente cuántos segundos debe esperar el cliente antes de intentar una nueva conexión. Esta postura transparente evita bucles infinitos de reintentos por parte de aplicaciones consumidoras mal configuradas. Para complementar esta implementación en entornos de producción, consulte también nuestros análisis técnicos sobre IA generando código inseguro? Cómo usar AST para escaneo y ¿Bundle pesado en React? Cómo aplicar code splitting eficiente.
Consideraciones Finales
Proteger las aplicaciones modernas contra sobrecargas repentinas ha dejado de ser un diferencial y se ha convertido en un requisito básico de la ingeniería de software. Como hemos visto, combinar el modelo dinámico de Token Bucket con la velocidad atómica de Redis resuelve el problema de escalabilidad y distribución de manera elegante.
Adoptar estas prácticas mitiga ataques no deseados, mejora la estabilidad general de la infraestructura y asegura que sistemas bajo alta carga, como detallamos en nuestro artículo sobre sistemas de alta carga y el uso del patrón Sidecar, continúen entregando respuestas rápidas y confiables a los usuarios legítimos.