Marcio Cunha

Cómo blindar APIs contra DoS usando rate limiting con Redis

Aprende a proteger tus APIs REST contra tráfico malicioso y ataques de denegación de servicio utilizando Redis y el algoritmo de token bucket en entornos distribuidos.

Marcio Cunha•7 min
También disponible en:EnglishPortuguês
Resumen
  • El algoritmo de token bucket permite ráfagas controladas de tráfico sin penalizar a usuarios legítimos.
  • Redis garantiza alta velocidad y consistencia de datos en arquitecturas distribuidas con múltiples nodos.
  • Los scripts de Lua ejecutados directamente en Redis evitan condiciones de carrera en operaciones concurrentes.
  • Las claves basadas en IP o tokens de autenticación aíslan el abuso de forma granular por cliente.
  • Las cabeceras HTTP estandarizadas mantienen al cliente informado sobre el consumo y los límites restantes.

El peligro invisible del tráfico descontrolado en APIs REST

Mantener una API REST abierta en internet es una invitación constante a problemas si no existe un control adecuado sobre quién accede a qué y con qué frecuencia. Imagina que tu aplicación web recién lanzada comienza a recibir miles de solicitudes por segundo procedentes de un script malicioso o de un robot de extracción de datos. En la práctica, esto significa que tus servidores sufrirán una sobrecarga severa, agotando las conexiones de bases de datos y dejando el sistema fuera de línea para los usuarios legítimos. Proteger tus endpoints críticos requiere una estrategia robusta de control de tasa, conocida en el mundo de la ingeniería de software como rate limiting, que funciona como un vigilante estricto en la puerta de un local concurrido.

En las arquitecturas modernas basadas en microservicios, donde decenas de instancias de una misma API se ejecutan simultáneamente detrás de un balanceador de carga, aplicar este control de forma aislada en cada servidor simplemente no funciona. Si cada servidor cuenta únicamente su propio tráfico, un atacante logrará multiplicar el límite de solicitudes por el número de servidores activos, evadiendo la protección por completo. Es precisamente en este escenario distribuido donde necesitamos una fuente central y veloz de verdad, capaz de registrar contadores de acceso en tiempo real y de forma atómica para todo el clúster de servidores. Antes de avanzar hacia la solución, vale la pena recordar cómo mitigamos otros cuellos de botella operativos, tal como discutimos al ver cómo proteger APIs contra fuerza bruta usando Redis y Lua para bloquear intentos repetidos de inicio de sesión.

Por qué el algoritmo de token bucket es ideal para APIs

Existen varias formas matemáticas de controlar el flujo de solicitudes, pero el algoritmo de token bucket, o cubo de tokens, destaca por su flexibilidad y excelente experiencia de usuario. Para entender el concepto en la práctica, imagina un cubo que recibe fichas o tokens a una tasa constante —digamos diez tokens por segundo— hasta alcanzar una capacidad máxima de cincuenta tokens. Cada vez que un cliente realiza una solicitud HTTP a tu API, el sistema retira un token del cubo y permite el acceso. Si el cubo está completamente vacío, la solicitud se rechaza de inmediato con un código de error HTTP 429 Too Many Requests, indicando que se ha superado el límite.

La gran ventaja de este modelo en comparación con un contador simple de ventana fija es que tolera ráfagas legítimas de tráfico. Si un usuario carga un panel complejo que dispara cinco solicitudes simultáneas, podrá hacerlo sin problemas siempre que haya fichas acumuladas en el cubo. Justo después de esta ráfaga, el cubo volverá a llenarse gradualmente a medida que pasa el tiempo, garantizando un equilibrio perfecto entre seguridad y usabilidad. Para implementar esta lógica de manera eficiente, necesitamos una base de datos en memoria que responda en fracciones de milisegundo, y es exactamente ahí donde entra el ecosistema de Redis.

Arquitectura distribuida con Redis y contadores atómicos

Redis es un almacenamiento de datos en memoria extremadamente rápido, estructurado en formato clave-valor, que funciona perfectamente como caché centralizada para nuestro sistema de control. Cuando múltiples servidores de API necesitan consultar y actualizar el saldo de tokens de un usuario en el mismo milisegundo, surge el desafío de la concurrencia y las condiciones de carrera. Si dos servidores leen el mismo valor de tokens al mismo tiempo, restan uno y lo guardan de vuelta, el saldo final será incorrecto, permitiendo más accesos de los permitidos. Para evitar este tipo de fallo silencioso, la ingeniería de software confía en la ejecución de scripts en el lenguaje Lua directamente dentro del servidor Redis.

Dado que Redis ejecuta scripts Lua de forma estricta y atómica, garantizamos que ninguna otra operación pueda interferir en el cubo de tokens mientras se realizan los cálculos de consumo y reabastecimiento. Dentro del script, calculamos el tiempo transcurrido desde la última solicitud, añadimos nuevos tokens de forma proporcional al tiempo pasado respetando la capacidad máxima del cubo, verificamos si hay saldo suficiente, restamos la unidad gastada y actualizamos el estado, todo en una única transacción ultrarrápida. En la práctica, esto significa que incluso bajo una avalancha masiva de accesos concurrentes desde diferentes regiones geográficas, el cálculo del limitador de tasa será matemáticamente perfecto y estará libre de inconsistencias.

Implementando la lógica con código funcional

Para ensuciarnos las manos, analicemos un fragmento de código en Node.js utilizando la biblioteca ioredis combinada con un script de Lua para gestionar el token bucket. Este enfoque centralizado garantiza que cualquier instancia de nuestra API pueda validar el acceso de forma instantánea, manteniendo el consumo de memoria bajo un control estricto en el servidor de caché.

const Redis = require('ioredis');
const redis = new Redis();

const tokenBucketScript = `
  local key = KEYS[1]
  local capacity = tonumber(ARGV[1])
  local refillRate = tonumber(ARGV[2])
  local now = tonumber(ARGV[3])
  local requested = tonumber(ARGV[4])

  local data = redis.call('HMGET', key, 'tokens', 'last_updated')
  local tokens = tonumber(data[1])
  local last_updated = tonumber(data[2])

  if not tokens then
    tokens = capacity
    last_updated = now
  else
    local elapsed = math.max(0, now - last_updated)
    local generated = elapsed * refillRate
    tokens = math.min(capacity, tokens + generated)
    last_updated = now
  end

  if tokens < requested then
    redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
    return 0
  end

  tokens = tokens - requested
  redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
  return 1
`;

async function checkRateLimit(clientId) {
  const capacity = 50;
  const refillRate = 5;
  const now = Date.now() / 1000;
  const requested = 1;
  const result = await redis.eval(tokenBucketScript, 1, clientId, capacity, refillRate, now, requested);
  return result === 1;
}

El código anterior demuestra cómo se recupera el estado del cubo, se recalcula en función del tiempo actual del sistema y se actualiza de forma atómica en Redis. El uso de hashes de Redis para almacenar el recuento de tokens y la marca de tiempo optimiza el consumo de espacio en memoria, permitiendo gestionar millones de usuarios activos concurrentes con un impacto casi nulo en la latencia de las solicitudes HTTP de tu API.

Buenas prácticas operativas y comunicación con el cliente

Implementar la restricción técnica de acceso es solo la mitad del trabajo; la otra mitad implica cómo tu aplicación comunica dicha restricción a los clientes legítimos y sistemas integrados. Siempre que una solicitud sea bloqueada por exceso de tráfico, la API debe devolver inmediatamente el código de estado HTTP 429, acompañado de cabeceras informativas estandarizadas. Estas cabeceras, conocidas como RateLimit-Limit, RateLimit-Remaining y RateLimit-Reset, permiten a los desarrolladores de terceros ajustar el ritmo de sus aplicaciones consumidoras de forma automatizada, evitando bloqueos innecesarios y mejorando la resiliencia general del ecosistema.

Asimismo, resulta fundamental elegir con criterio la clave de identificación utilizada para aplicar el límite. Dependiendo del modelo de negocio de tu aplicación, puedes limitar el acceso basándote en la dirección IP del cliente, el token de autenticación JWT o una clave de API corporativa. Para rutas públicas de inicio de sesión o recuperación de contraseña, la IP es indispensable para frenar ataques de fuerza bruta, mientras que las rutas autenticadas de alto rendimiento funcionan mejor cuando se vinculan directamente al identificador del plan del usuario. Monitorear el comportamiento de estas claves mediante métricas en tiempo real garantiza que puedas ajustar los límites operativos antes de que un pico inesperado afecte la estabilidad de la infraestructura. Para complementar esta implementación en entornos de producción, consulte también nuestros análisis técnicos sobre Cómo eliminar el Layout Shift al cargar fuentes personalizadas con CSS y Cómo crear microservicios resilientes con circuit breaker.

Consideraciones finales sobre resiliencia en microservicios

Blindar las APIs REST contra abusos, tráfico malicioso y picos repentinos de carga ha dejado de ser un diferencial estético para convertirse en un requisito básico de supervivencia en cualquier aplicación corporativa moderna. Al combinar la velocidad inigualable de Redis con la flexibilidad matemática del algoritmo de token bucket, creamos una barrera defensiva robusta y distribuida que protege los recursos informáticos sin penalizar a los usuarios legítimos. La inversión en una arquitectura de control de tasa bien planificada se amortiza rápidamente en forma de mayor estabilidad operativa, reducción de costos en servidores sobreaprovisionados y una experiencia de usuario consistentemente fluida, sin importar el volumen de accesos recibidos.