Rate Limiting

El rate limiting es una técnica fundamental de control de tráfico que limita el número de solicitudes que un cliente puede realizar a una API o servicio web dentro de una ventana de tiempo específica, protegiendo la infraestructura contra la sobrecarga, los ataques de denegación de servicio (DoS/DDoS), el abuso automatizado, el scraping malicioso y el uso inadecuado de recursos computacionales limitados. En un escenario donde las APIs modernas atienden millones de solicitudes por segundo provenientes de aplicaciones móviles, integraciones de socios, bots legítimos y potencialmente atacantes maliciosos, la ausencia de un rate limiting adecuado puede resultar en costos operativos explosivos, una degradación severa del rendimiento para usuarios legítimos, vulnerabilidades a ataques de credential stuffing y fuerza bruta, e incluso la indisponibilidad total del servicio. La implementación eficaz de rate limiting requiere una comprensión profunda de diferentes algoritmos (token bucket, leaky bucket, fixed window, sliding window), consideraciones sobre la arquitectura distribuida donde múltiples servidores necesitan compartir contadores de tasa, estrategias de identificación de clientes (IP, API key, JWT, sesión), políticas diferenciadas por nivel de usuario (free, premium, enterprise), y mecanismos de comunicación clara a través de HTTP headers estandarizados que informan a los clientes sobre los límites, el consumo actual y el tiempo de reinicio. Este artículo explora algoritmos, patrones de implementación, herramientas y best practices para construir sistemas robustos de rate limiting.

Algoritmos de Rate Limiting

1. Token Bucket (Más Común)

      # Concepto: Bucket con capacidad máxima de tokens
      # Los tokens se añaden a tasa constante
      # Cada request consume 1 token
      # Si el bucket está vacío, el request es rechazado
      Capacidad: 100 tokens
      Tasa de recarga: 10 tokens/segundo
      Ventajas:
      - Permite bursts controlados (bucket lleno)
      - Simple de implementar
      - Flexible para diferentes patrones de tráfico
      Desventajas:
      - Puede permitir bursts que sobrecargan el sistema
      - Necesita tracking de la última recarga
      Implementación:
      class TokenBucket {
      constructor(capacity, refillRate) {
      this.capacity = capacity;
      this.tokens = capacity;
      this.refillRate = refillRate;
      this.lastRefill = Date.now();
      }
      consume(count = 1) {
      this.refill();
      if (this.tokens >= count) {
      this.tokens -= count;
      return true;
      }
      return false;
      }
      refill() {
      const now = Date.now();
      const elapsed = (now - this.lastRefill) / 1000;
      const tokensToAdd = elapsed * this.refillRate;
      this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
      this.lastRefill = now;
      }
      }
      

2. Leaky Bucket

      # Concepto: Queue con vaciado constante
      # Los requests entran en el bucket (queue)
      # Procesados a tasa constante (leak)
      # Si la queue está llena, los requests son rechazados
      Ventajas:
      - Suaviza los bursts (traffic shaping)
      - Output rate constante y previsible
      - Protege el backend de spikes
      Desventajas:
      - Puede añadir latencia (queueing)
      - Complejidad de implementación
      Uso:
      - Traffic shaping
      - Network gateways
      - Cuando el output rate constante es crítico
      

3. Fixed Window Counter

      # Concepto: Contador por ventana de tiempo fija
      # Ejemplo: 100 requests por minuto
      # Reinicio al inicio de cada minuto (XX:00, XX:01, XX:02...)
      Ventajas:
      - Extremadamente simple
      - Memory efficient
      - Fácil de entender
      Desventajas:
      - Edge case: 200 requests en 1 segundo
      (100 al final del minuto 1, 100 al inicio del minuto 2)
      - Permite bursts en el límite de las ventanas
      Implementación Redis:
      INCR user:123:2024-01-15:14:30
      EXPIRE user:123:2024-01-15:14:30 60
      GET user:123:2024-01-15:14:30  # Si > 100, reject
      

4. Sliding Window Log

      # Concepto: Log de timestamps de requests
      # Elimina los requests fuera de la ventana
      # Cuenta los requests dentro de la ventana deslizante
      Ventajas:
      - Precisión perfecta
      - Sin edge cases de fixed window
      - Distribuye la tasa uniformemente
      Desventajas:
      - Memory intensive (almacena todos los timestamps)
      - El rendimiento se degrada con high traffic
      Implementación Redis (Sorted Set):
      ZADD user:123 <timestamp> <request-id>
      ZREMRANGEBYSCORE user:123 0 <timestamp-60s>  # Elimina los antiguos
      ZCARD user:123  # Count requests
      Si > 100, reject
      

5. Sliding Window Counter (Híbrido)

      # Concepto: Combina fixed window con sliding
      # Usa contadores de ventanas anteriores con peso
      # Estima la tasa en una sliding window
      Ejemplo: Límite 100/minuto
      Ventana actual (14:30): 70 requests
      Ventana anterior (14:29): 90 requests
      Transcurrido en la ventana actual: 40s (66.7%)
      Estimación: 90 * (1 - 0.667) + 70 = 30 + 70 = 100
      Ventajas:
      - Precisión cercana a sliding log
      - Memory efficient (solo 2 contadores)
      - Suaviza los bursts
      Desventajas:
      - Estimación (no exacto)
      - Más complejo que fixed window
      

Distributed Rate Limiting

      # Problema: Múltiples servidores necesitan compartir el state
      # Soluciones:
      1. Redis Centralizado (Más Común)
      const redis = require('redis');
      const client = redis.createClient();
      async function checkRateLimit(userId) {
      const key = \`rate:\$:\${getCurrentWindow()}\`;
      const count = await client.incr(key);
      if (count === 1) {
      await client.expire(key, 60); // 60 seconds
      }
      return count <= 100; // Limit: 100/min
      }
      2. Redis Lua Script (Atomic)
      const luaScript = \`
      local key = KEYS[1]
      local limit = tonumber(ARGV[1])
      local current = redis.call('incr', key)
      if current == 1 then
      redis.call('expire', key, ARGV[2])
      end
      if current > limit then
      return 0
      end
      return 1
      \`;
      3. Sticky Sessions + Local Counters
      - Enrutar al usuario siempre al mismo server
      - Counter local en el servidor
      - Problema: No funciona bien con auto-scaling
      4. Gossip Protocol
      - Los servidores comparten el state vía gossip
      - Eventual consistency
      - Más complejo, usado en high scale
      

HTTP Headers Estándar

      # Standards (RFCs)
      X-RateLimit-Limit: 100
      X-RateLimit-Remaining: 45
      X-RateLimit-Reset: 1640000000  # Unix timestamp
      # Cuando se excede el límite
      HTTP/1.1 429 Too Many Requests
      Retry-After: 60  # Segundos hasta el retry
      X-RateLimit-Limit: 100
      X-RateLimit-Remaining: 0
      X-RateLimit-Reset: 1640000060
      {
      "error": "Rate limit exceeded",
      "retryAfter": 60,
      "limit": 100
      }
      # Estilo GitHub (más informativo)
      X-RateLimit-Limit: 5000
      X-RateLimit-Remaining: 4999
      X-RateLimit-Reset: 1372700873
      X-RateLimit-Used: 1
      X-RateLimit-Resource: core
      

Implementación con Express.js

      const rateLimit = require('express-rate-limit');
      const RedisStore = require('rate-limit-redis');
      const redis = require('redis');
      const client = redis.createClient();
      // Basic rate limiter
      const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 requests per windowMs
      standardHeaders: true, // Return rate limit info in headers
      legacyHeaders: false,
      message: 'Too many requests, please try again later.'
      });
      app.use('/api/', limiter);
      // Redis-based distributed rate limiting
      const distributedLimiter = rateLimit({
      store: new RedisStore({
      client: client,
      prefix: 'rate-limit:',
      }),
      windowMs: 60 * 1000,
      max: 10,
      standardHeaders: true,
      });
      // Different limits per route
      const authLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 5, // Stricter for auth endpoints
      skipSuccessfulRequests: true, // Don't count successful logins
      });
      app.post('/api/login', authLimiter, loginHandler);
      // Custom key function (rate limit by user ID instead of IP)
      const userLimiter = rateLimit({
      windowMs: 60 * 1000,
      max: 100,
      keyGenerator: (req) => req.user.id, // Requires auth middleware
      });
      

Tiered Rate Limiting

      // Different limits based on user tier
      function getRateLimit(user) {
      const tiers = {
      free: { windowMs: 3600000, max: 100 },      // 100/hour
      basic: { windowMs: 3600000, max: 1000 },    // 1000/hour
      premium: { windowMs: 3600000, max: 10000 }, // 10k/hour
      enterprise: { windowMs: 3600000, max: 100000 } // 100k/hour
      };
      return tiers[user.tier] || tiers.free;
      }
      app.use(async (req, res, next) => {
      const user = await getUserFromToken(req);
      const limits = getRateLimit(user);
      const limiter = rateLimit({
      ...limits,
      keyGenerator: () => user.id,
      });
      limiter(req, res, next);
      });
      

Herramientas y Servicios

  • Redis: Contadores distribuidos, expire automático
  • Kong: API Gateway con plugin de rate limiting
  • Nginx rate limiting: limit_req_zone, limit_conn_zone
  • Cloudflare: Rate limiting a nivel de CDN
  • AWS API Gateway: Throttling integrado
  • express-rate-limit: Middleware de Express
  • Tyk: API gateway open-source

Best Practices

  • Elige el algoritmo adecuado: Token bucket para APIs generales, sliding window para precisión
  • Límites diferenciados: Endpoints de auth más restrictivos, read-only más permisivos
  • Comunicación clara: Headers informativos, mensajes de error útiles
  • Whitelist: IPs de socios confiables, health checks
  • Monitoring: Alertar cuando los usuarios alcanzan los límites con frecuencia
  • Graceful degradation: Devolver cached data si es posible
  • Distributed state: Usar Redis para deployments multi-servidor
  • Cost-based limiting: Las operaciones costosas consumen más tokens

Recomendaciones

Para las APIs modernas, implementa token bucket con Redis para distributed rate limiting. Usa sliding window counter cuando necesites precisión sin overhead de memoria. Configura límites diferenciados: 5 req/min para login, 100 req/min para lectura, 10 req/min para operaciones de escritura. Devuelve siempre headers informativos e implementa retry logic con exponential backoff en los clientes.