StackPractices
intermediate Por Mathias Paulenko

Implementar Rate Limiting de APIs con Redis

Protege APIs de abuso usando algoritmos de token bucket y ventana deslizante con Redis, incluyendo manejo de bursts, coordinacion distribuida y headers custom para feedback al cliente

Previene abuso de APIs y asegura distribucion justa de recursos usando rate limiters respaldados por Redis. Consulta Guía de Seguridad para estrategias generales de protección de APIs. Esta recipe implementa algoritmos de token bucket y ventana deslizante con coordinacion distribuida, headers custom de rate limit y configuracion por endpoint para APIs de produccion.

Cuando Usar Esto

  • APIs publicas necesitan proteccion contra brute force y scraping
  • Diferentes tiers de usuarios requieren diferentes rate limits (free vs paid)
  • Multiples nodos de API deben compartir estado de rate limit consistentemente

Solucion

1. Algoritmo de Token Bucket

// rate-limiter/TokenBucket.ts
import { Redis } from 'ioredis';

class TokenBucketLimiter {
  private redis: Redis;

  constructor(redis: Redis) {
    this.redis = redis;
  }

  async check(key: string, capacity: number, refillRate: number): Promise<{ allowed: boolean; remaining: number; resetTime: number }> {
    const now = Date.now();
    const bucketKey = `ratelimit:token:${key}`;

    const luaScript = `
      local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'lastRefill')
      local tokens = tonumber(bucket[1]) or ARGV[1]
      local lastRefill = tonumber(bucket[2]) or ARGV[3]
      local now = tonumber(ARGV[3])
      local capacity = tonumber(ARGV[1])
      local refillRate = tonumber(ARGV[2])
      local interval = tonumber(ARGV[4])

      local elapsed = now - lastRefill
      local refill = math.floor(elapsed / interval) * refillRate
      tokens = math.min(capacity, tokens + refill)

      if tokens >= 1 then
        tokens = tokens - 1
        redis.call('HMSET', KEYS[1], 'tokens', tokens, 'lastRefill', now)
        redis.call('EXPIRE', KEYS[1], 3600)
        return {1, tokens, now + interval}
      else
        redis.call('HMSET', KEYS[1], 'tokens', tokens, 'lastRefill', lastRefill)
        redis.call('EXPIRE', KEYS[1], 3600)
        return {0, tokens, lastRefill + interval}
      end
    `;

    const result = await this.redis.eval(luaScript, 1, bucketKey, capacity, refillRate, now, 1000);
    const [allowed, remaining, resetTime] = result as [number, number, number];

    return {
      allowed: allowed === 1,
      remaining,
      resetTime,
    };
  }
}

2. Contador de Ventana Deslizante

// rate-limiter/SlidingWindow.ts
class SlidingWindowLimiter {
  private redis: Redis;

  constructor(redis: Redis) {
    this.redis = redis;
  }

  async check(key: string, limit: number, windowMs: number): Promise<{ allowed: boolean; remaining: number; resetTime: number }> {
    const now = Date.now();
    const windowKey = `ratelimit:window:${key}`;
    const windowStart = now - windowMs;

    const pipeline = this.redis.pipeline();
    pipeline.zremrangebyscore(windowKey, 0, windowStart);
    pipeline.zcard(windowKey);
    pipeline.zadd(windowKey, now, `${now}-${Math.random()}`);
    pipeline.pexpire(windowKey, windowMs);

    const [, [currentCount], ,] = await pipeline.exec() as [unknown, [number], unknown, unknown];

    if (currentCount < limit) {
      return {
        allowed: true,
        remaining: limit - currentCount - 1,
        resetTime: now + windowMs,
      };
    }

    await this.redis.zremrangebyrank(windowKey, -1, -1);
    const oldest = await this.redis.zrange(windowKey, 0, 0, 'WITHSCORES');
    const resetTime = oldest.length > 0 ? Number(oldest[1]) + windowMs : now + windowMs;

    return {
      allowed: false,
      remaining: 0,
      resetTime,
    };
  }
}

3. Middleware de Express con Headers

// middleware/rateLimit.ts
import { Request, Response, NextFunction } from 'express';

function rateLimit(limiter: TokenBucketLimiter, options: { capacity: number; refillRate: number }) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const key = `${req.ip}:${req.route?.path || req.path}`;
    const result = await limiter.check(key, options.capacity, options.refillRate);

    res.setHeader('X-RateLimit-Limit', String(options.capacity));
    res.setHeader('X-RateLimit-Remaining', String(Math.max(0, result.remaining)));
    res.setHeader('X-RateLimit-Reset', String(Math.ceil(result.resetTime / 1000)));

    if (!result.allowed) {
      res.status(429).json({
        error: 'Too Many Requests',
        retryAfter: Math.ceil((result.resetTime - Date.now()) / 1000),
      });
      return;
    }

    next();
  };
}

app.use('/api/public', rateLimit(limiter, { capacity: 100, refillRate: 10 }));
app.use('/api/premium', rateLimit(limiter, { capacity: 1000, refillRate: 100 }));

4. Configuracion por Tier de Usuario

// rate-limiter/TierConfig.ts
interface RateLimitConfig {
  capacity: number;
  refillRate: number;
}

const tierLimits: Record<string, RateLimitConfig> = {
  free: { capacity: 100, refillRate: 10 },
  pro: { capacity: 1000, refillRate: 100 },
  enterprise: { capacity: 10000, refillRate: 1000 },
};

async function tieredRateLimit(req: Request, res: Response, next: NextFunction) {
  const userId = req.headers['x-user-id'] as string;
  const tier = userId ? await getUserTier(userId) : 'free';
  const config = tierLimits[tier] || tierLimits.free;

  const key = `${tier}:${userId || req.ip}`;
  const result = await limiter.check(key, config.capacity, config.refillRate);

  if (!result.allowed) {
    res.status(429).json({ error: 'Rate limit exceeded', tier });
    return;
  }

  next();
}

Como Funciona

  • Token bucket permite bursts hasta la capacidad mientras mantiene tasa promedio
  • Sliding window provee limites mas estrictos trackeando timestamps exactos de requests
  • Scripts Lua de Redis garantizan operaciones atomicas a traves de nodos distribuidos
  • Headers de respuesta informan a clientes de cuota restante y tiempo de reset

Consideraciones de Produccion

  • Usa Redis Cluster para estado de rate limit de alta disponibilidad
  • Implementa circuit breaker alrededor de Redis para fail open si el cache cae
  • Logea violaciones de rate limit para monitoreo de seguridad y deteccion de abuso

Errores Comunes

  • Rate limitar solo por IP, bloqueando usuarios legitimos detras de NAT
  • No manejar fallos de Redis gracefulmente, causando outages de API
  • Retornar 429 sin headers Retry-After, dejando clientes adivinando
  • Middleware de Express: Para patrones específicos de Express, consulta Patrones de Middleware de Express.

Troubleshooting

  • 5xx errors under load: check rate limits, connection pools, and downstream timeouts.
  • CORS errors in the browser: confirm allowed origins, methods, and headers. Preflight requests must return the right headers before the actual request.
  • Unexpected 404s: verify route definitions, path parameters, and base paths. Watch for trailing slashes and URL encoding differences.
  • Authentication failures: validate token expiry, signature algorithms, and clock skew. Log rejected tokens without exposing secrets.
  • Slow response times: profile the slowest percentiles.

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de throttling y redis para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica implementar rate limiting de apis con redis cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Errores Comunes en Producción

  • Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
  • Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
  • Codificar valores fijos que deberían ser configurables por entorno.
  • Olvidar agregar logging y monitoreo en cada paso.
  • Desplegar sin plan de rollback ni estrategia de backup probada.
  • Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
  • No documentar la versión y configuración usadas en producción.
  • Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.

Preguntas frecuentes

Token bucket vs sliding window: ¿cuál debería usar?

Token bucket permite bursts controlados y es más eficiente con operaciones de Redis (una call de Lua script por request). Sliding window es más estricto y mejor para requerimientos de compliance exacto (ej., APIs financieras que deben enforcear conteos exactos de requests por minuto). Token bucket recarga a una tasa fija, así que un cliente que espera puede acumular tokens para un burst. Sliding window cuenta requests en una ventana de tiempo rolling, previniendo bursts enteramente. Para la mayoría de las APIs, token bucket con un burst size razonable (2x la tasa steady-state) es suficiente.

¿Cómo manejo rate limits a través de microservicios?

Usa una instancia Redis compartida o un servicio dedicado de rate limiting con APIs gRPC/HTTP. Cada servicio llama al rate limiter antes de procesar el request. Para deployments distribuidos, usa Redis Cluster para evitar un single point of failure. Alternativamente, deploya un API gateway (Kong, Envoy) que maneje rate limiting centralmente. Para calls service-to-service, implementa un token-bucket por par de servicios en lugar de por cliente para prevenir que un servicio downstream agote el quota de otro.

¿Cómo manejo fallos de Redis sin bloquear todo el tráfico?

Implementa un circuit breaker alrededor de las calls a Redis. Si Redis no está disponible, fail open (permite el request) y logea un warning. Fail closed (bloquear todo el tráfico) causa outages en cascada. Usa un fallback in-memory local (ej., un counter simple con lru-cache) para outages cortos de Redis. Setea un timeout en los comandos de Redis (ej., 50ms) para que un Redis lento no bloquee el procesamiento de requests. Monitorea la salud de Redis y alerta cuando el circuit breaker está open.

¿Cómo seteo rate limits por tier (free, pro, enterprise)?

Almacena el tier en la API key o JWT claims. Busca la configuración de rate limit del tier desde una base de datos o archivo de config. Usa prefijos de Redis key diferentes por tier: rate:free:{key}, rate:pro:{key}, rate:enterprise:{key}. Aplica diferentes bucket sizes y refill rates por tier. Retorna el tier en headers de respuesta (X-RateLimit-Tier: pro) para que los clientes puedan debuggear sus límites. Actualiza la configuración de tier sin redeployar leyendo desde un config service o base de datos.

¿Cómo testeo lógica de rate limiting?

Escribe unit tests para el Lua script usando redis-cli --eval con mock keys. Testea boundary conditions: exactamente en el límite, uno sobre el límite, refill después de esperar. Para tests de integración, usa ioredis-mock o una instancia Redis real en un Docker container. Simula requests concurrentes con Promise.all para verificar atomicidad. Testea escenarios de fallo de Redis matando el proceso de Redis mid-test y verificando que el circuit breaker abra. Load testea con k6 o artillery para verificar que el rate limiter maneje el throughput esperado.

¿Cómo rate limito conexiones WebSocket?

Rate limita por connection ID, no solo IP. Trackea conexiones activas por usuario en Redis con un sorted set (ZADD ws:connections timestamp connectionId). Enforcea un límite de conexiones máximas por usuario. Para message rate, cuenta mensajes por conexión con un token bucket: rate:ws:{connectionId}. Desconecta clientes que excedan el message rate con un close code 1008 (policy violation). Limpia entries de conexión al desconectar con ZREMRANGEBYSCORE.

¿Cómo manejo headers de rate limit en un gateway?

Setea headers estándar: X-RateLimit-Limit (máx requests por ventana), X-RateLimit-Remaining (requests restantes), X-RateLimit-Reset (Unix timestamp cuando la ventana resetea), y Retry-After (segundos a esperar, solo en respuestas 429). Usa el prefijo de header RateLimit-* del draft IETF (ej., RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) para clientes modernos. No expongas configuración interna de rate limit (bucket size, refill rate) — solo los límites efectivos que el cliente ve.

¿Cómo implemento rate limiting distribuido sin Redis?

Para entornos sin Redis, usa un token bucket en una base de datos compartida (PostgreSQL SELECT FOR UPDATE, DynamoDB conditional writes). El downside es mayor latencia — cada check de rate limit requiere un database round trip. Para eventual consistency, usa un token bucket local por instancia y sin croniza periódicamente con un store central. Esto permite bursts cortos sobre el límite pero converge a la tasa correcta over time. Para rate limiting a nivel CDN, usa las rate limiting rules de Cloudflare o AWS WAF rate-based rules — estas bloquean requests antes de que lleguen a tu servidor.

¿Cómo manejo rate limiting para requests autenticados vs no autenticados?

Usa diferentes rate limit keys: rate:ip:{ip} para no autenticados y rate:user:{userId} para autenticados. Aplica diferentes límites: no autenticados gets 10 req/min, autenticados gets 100 req/min. Chequea autenticación primero — si el usuario está autenticado, skipea el limiter basado en IP. Para APIs con tiers free y paid, usa rate:free:{userId} y rate:pro:{userId} con diferentes bucket sizes. Retorna el status de autenticación en headers (X-RateLimit-Authenticated: true) para que los clientes puedan debuggear por qué hits a diferentes límites.

¿Cómo implemento un algoritmo sliding window log en Redis?

Usa un sorted set por cliente: ZADD rate:log:{key} timestamp request_id. Remueve entries fuera de la ventana: ZREMRANGEBYSCORE rate:log:{key} 0 (now - window). Cuenta entries restantes: ZCARD rate:log:{key}. Si el count excede el límite, rechaza el request. Este approach usa más memoria que token bucket pero provee conteos exactos de requests en cualquier ventana de tiempo. Agrega un TTL en la key para auto-expirar clientes inactivos. Para APIs de alto tráfico, prefiere token bucket — sliding window log almacena cada request ID, lo que puede consumir significant Redis memory.