Saltar al contenido principal

Rate Limiting

Rate Limiting (limitación de tasa) es una técnica que controla cuántas peticiones puede hacer un cliente a un servidor en un periodo de tiempo determinado. Cuando se supera ese límite, el servidor rechaza o retrasa las peticiones adicionales.

Sirve para:

  • Prevenir ataques de fuerza bruta: limitar los intentos de login evita que un atacante pruebe millones de contraseñas.
  • Prevenir ataques DoS/DDoS: reduce el impacto de un volumen masivo de peticiones.
  • Evitar el scraping abusivo: protege los datos de la aplicación frente a bots.
  • Controlar el abuso de la API: garantiza un uso justo entre todos los usuarios.
  • Reducir costes: en APIs de pago (IA, SMS, email…), evita consumos inesperados.

Conceptos clave en rate limiting:

  • Ventana de tiempo (window): el periodo durante el cual se cuentan las peticiones. Por ejemplo, 15 minutos.
  • Límite (max): número máximo de peticiones permitidas en esa ventana.
  • Identificador del cliente: cómo se identifica quién hace la petición (IP, token, usuario…).
  • Código de respuesta: cuando se supera el límite, se responde con 429 Too Many Requests.

Algoritmos de Rate Limiting

Fixed Window (ventana fija)

Cuenta las peticiones en intervalos fijos (por ejemplo, cada minuto). Sencillo pero con un problema: puede permitir el doble del límite en el cambio de ventana.

[0s ──────── 60s] [60s ──────── 120s]
100 peticiones 100 peticiones

Un atacante puede enviar 100 peticiones justo al final de una ventana y otras 100 al inicio de la siguiente.

Sliding Window (ventana deslizante)

Cuenta las peticiones en los últimos N segundos desde el momento actual. Más preciso, sin el problema del cambio de ventana.

Token Bucket

Hay un "cubo" (bucket) con fichas. Cada petición consume una ficha. Las fichas se reponen a un ritmo constante. Permite ráfagas cortas de tráfico.

Leaky Bucket

Las peticiones entran en una cola y se procesan a un ritmo constante. Suaviza el tráfico pero añade latencia en ráfagas.

Rate Limiting en Node.js

express-rate-limit

La librería express-rate-limit es la más popular para Express.

npm install express-rate-limit

Limitar todas las rutas (uso básico):

import { rateLimit } from 'express-rate-limit';

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // Ventana de 15 minutos
max: 100, // Máximo 100 peticiones por IP
standardHeaders: true, // Devuelve info en cabeceras RateLimit-*
legacyHeaders: false, // Desactiva las cabeceras X-RateLimit-* antiguas
message: {
error: 'Demasiadas peticiones. Por favor, inténtalo más tarde.'
}
});

app.use(limiter); // Aplica a todas las rutas

Proteger el endpoint de login (más estricto):

const loginLimiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutos
max: 5, // Máximo 5 intentos por IP
message: {
error: 'Demasiados intentos de login. Cuenta bloqueada temporalmente.'
},
skipSuccessfulRequests: true, // No cuenta los intentos exitosos
});

app.post('/login', loginLimiter, authController.login);

Distintos límites por ruta:

// API pública: más restrictiva
const apiPublicaLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 30,
});

// API autenticada: más permisiva
const apiPrivadaLimiter = rateLimit({
windowMs: 60 * 1000,
max: 200,
});

app.use('/api/public', apiPublicaLimiter);
app.use('/api/v1', verificarToken, apiPrivadaLimiter);

rate-limit-redis

Por defecto, express-rate-limit almacena los contadores en memoria, lo que significa que el estado se pierde al reiniciar y no funciona con múltiples instancias (varios servidores / procesos). Para entornos de producción con escalado horizontal, se usa Redis como almacén compartido.

Para ello, necesitamos instalar rate-limit-redis y ioredis:

npm install rate-limit-redis ioredis
import { rateLimit } from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';
import Redis from 'ioredis';

const redisClient = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});

const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
store: new RedisStore({
sendCommand: (...args) => redisClient.call(...args),
}),
});

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

bottleneck

Para controlar el ritmo de llamadas salientes podemos utilizar bottleneck. Útil cuando tu backend llama a APIs externas con límite de tasa (servicios de terceros, IA, SMS…):

npm install bottleneck
import Bottleneck from "bottleneck";

const limiter = new Bottleneck({
minTime: 200, // Al menos 200ms entre llamadas
maxConcurrent: 5, // Máximo 5 llamadas simultáneas
});

// En lugar de llamar directamente:
// const resultado = await llamadaAServicioExterno(param);

// Se pasa por el limiter:
const resultado = await limiter.schedule(() => llamadaAServicioExterno(param));

Ejemplo completo: API con rate limiting por usuario

const express = require('express');
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const app = express();
const redis = new Redis();

// Función para identificar al cliente: por token si está autenticado, por IP si no
const identificarCliente = (req) => {
return req.user?.id || req.ip;
};

const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
keyGenerator: identificarCliente, // Límite por usuario, no por IP
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
}),
handler: (req, res, next, options) => {
res.status(options.statusCode).json({
error: 'Límite de peticiones superado',
resetAt: new Date(Date.now() + options.windowMs).toISOString(),
});
},
});

app.use('/api', verificarToken, apiLimiter);

Buenas prácticas

  • Aplica límites más estrictos en rutas sensibles: login, registro, recuperación de contraseña, envío de emails.
  • Distingue entre usuarios autenticados (mayor límite) y anónimos (menor límite).
  • Devuelve la cabecera Retry-After para que los clientes sepan cuándo reintentar.
  • Combina rate limiting con CAPTCHA en formularios públicos para mayor protección.
  • En producción, usa siempre un almacén persistente (Redis) si tienes más de un proceso o servidor.
  • No uses rate limiting como única defensa frente a ataques de fuerza bruta. Complementa con bloqueo de cuentas y alertas.

Cabeceras de respuesta

Cuando se configura express-rate-limit con standardHeaders a true, devuelve automáticamente:

RateLimit-Limit: 100
RateLimit-Remaining: 43
RateLimit-Reset: 1710000000

Cuando se supera el límite:

HTTP/1.1 429 Too Many Requests
Retry-After: 900