Una API sin rate limiting es una puerta abierta. Cualquier cliente puede hacer miles de requests por segundo, tumbar el servidor o consumir recursos que otros usuarios necesitan. Pero un rate limiting excesivo bloquea usuarios legítimos que están usando la API normalmente.

Los algoritmos de rate limiting

Tres enfoques dominan la práctica:

Fixed Window

Cuenta requests dentro de una ventana fija (ej: 100 requests por minuto). Simple, pero tiene el problema del "edge burst": 100 requests en el segundo 59 de la ventana + 100 en el segundo 0 de la siguiente = 200 requests en 2 segundos.

Sliding Window Log

Mantiene el timestamp de cada request en la ventana. Más preciso, pero consume memoria proporcional al número de requests.

Token Bucket

El más usado en producción. Los tokens se agregan a intervalos regulares. Cada request consume un token. Cuando se acaban los tokens, el request es rechazado. Permite bursts controlados.

// Token bucket con Redis
async function isRateLimited(
  key: string,
  maxTokens: number,
  refillRate: number
): Promise<boolean> {
  const now = Date.now()
  const bucket = await redis.hgetall(`ratelimit:${key}`)

  if (!bucket.tokens) {
    // Primer request: crea el bucket
    await redis.hset(`ratelimit:${key}`, {
      tokens: maxTokens - 1,
      lastRefill: now
    })
    await redis.expire(`ratelimit:${key}`, 60)
    return false
  }

  // Refill de tokens según el tiempo transcurrido
  const elapsed = now - Number(bucket.lastRefill)
  const refill = Math.floor(elapsed / 1000 * refillRate)
  const tokens = Math.min(maxTokens, Number(bucket.tokens) + refill)

  if (tokens <= 0) return true  // rate limited

  await redis.hset(`ratelimit:${key}`, {
    tokens: tokens - 1,
    lastRefill: now
  })

  return false
}

Por clave: IP, usuario o API key

La clave de rate limit define a quién se limita:

  • Por IP: protege contra bots y abuso anónimo. Básico, pero no diferencia usuarios legítimos de IPs compartidas (CGNAT, VPNs).
  • Por usuario autenticado: más justo. Los usuarios pagan por plan, y el rate limit acompaña el plan.
  • Por API key: para APIs públicas. Cada desarrollador tiene su bucket, y el abuso no afecta a otros.
  • Compuesto: límite global por IP + límite individual por usuario. Protege contra DDoS y abuso simultáneamente.

Headers HTTP que comunican el estado

Los clientes necesitan saber cuándo están cerca del límite. Estos headers son estándar del mercado:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 23
X-RateLimit-Reset: 1711234567
Retry-After: 30  // cuando retorna 429

Retry-After es obligatorio en responses 429. Sin él, el cliente no sabe cuándo intentar de nuevo y puede entrar en loop de retries.

Implementación con express-rate-limit

Para Node.js, la implementación estándar:

import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'

const limiter = rateLimit({
  windowMs: 60 * 1000,  // 1 minuto
  max: 100,             // 100 requests por ventana
  standardHeaders: true,
  legacyHeaders: false,
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args),
  }),
  keyGenerator: (req) => req.user?.id || req.ip,
  handler: (req, res) => {
    res.status(429).json({
      error: {
        code: 'RATE_LIMITED',
        message: 'Demasiadas requests. Intenta de nuevo más tarde.',
        retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
      }
    })
  }
})

app.use('/api/', limiter)

Rate limiting diferenciado por ruta

No toda ruta tiene el mismo costo. Login tiene costo alto (bcrypt). Los listados son baratos. Aplica límites diferentes:

// Login: 5 intentos por minuto (protege contra brute force)
app.use('/api/auth/login', rateLimit({ windowMs: 60000, max: 5 }))

// Listados: 100 por minuto
app.use('/api/products', rateLimit({ windowMs: 60000, max: 100 }))

// Upload: 10 por hora
app.use('/api/upload', rateLimit({ windowMs: 3600000, max: 10 }))

Qué hacer cuando se alcanza el rate limit

Además del 429 con Retry-After, implementa graceful degradation: cachea la response por más tiempo, o retorna datos parciales cuando estén disponibles. El usuario recibe algo en vez de un error puro.