Volver al blog
Node.jsNext.jsLimitación de velocidadSeguridad de APIDDoSFuerza brutaRedis

Limitación de velocidad de API y protección contra fuerza bruta en Node.js y Next.js [2026]

Por qué importa la limitación de velocidad

Cada endpoint de API pública es un vector de ataque potencial. Sin limitación de velocidad, un único cliente malicioso puede:

  • Atacar por fuerza bruta tus endpoints de autenticación (probando miles de contraseñas por minuto)
  • Extraer (scrapear) toda tu base de datos en minutos (abusando de endpoints GET paginados)
  • Agotar recursos (CPU, conexiones de base de datos, memoria) con una inundación de tipo DDoS
  • Inundar de spam tus formularios de contacto, flujos de registro o secciones de comentarios
  • Hacerte gastar dinero (si pagas por llamada de API a un servicio de terceros)

La limitación de velocidad limita el número de peticiones que un cliente puede realizar dentro de una ventana de tiempo. Cuando se implementa correctamente, detiene cada uno de estos ataques sin degradar la experiencia de los usuarios legítimos.

Cómo elegir un algoritmo de limitación de velocidad

Existen tres algoritmos principales. Cada uno tiene sus ventajas e inconvenientes:

| Algoritmo | Comportamiento | Ideal para | |-----------|----------|----------| | Ventana fija (Fixed Window) | Cuenta peticiones por ventana alineada al reloj (p. ej., 100 req/min que se reinician cada minuto a las :00) | Endpoints sencillos, aplicaciones de bajo tráfico | | Registro de ventana deslizante (Sliding Window Log) | Registra las marcas de tiempo de cada petición; elimina las entradas más antiguas que la ventana | Escenarios que requieren precisión | | Ventana deslizante (Sorted Set de Redis) | Cuenta peticiones en una ventana de tiempo móvil usando un sorted set de Redis | Sistemas distribuidos, producción | | Cubo de fichas (Token Bucket) | Las fichas se reponen a un ritmo constante; se permiten ráfagas hasta la capacidad del cubo | APIs con patrones de tráfico en ráfagas | | GCRA (Generic Cell Rate Algorithm) | Limita la velocidad con un uso mínimo de memoria por clave (usado por las librerías de limitación de velocidad) | Alto rendimiento, restricciones de memoria |

Recomendación para producción: usa ventana deslizante con Redis o GCRA (implementados por @upstash/ratelimit o rate-limit-redis). Estos evitan el problema del "pico de tráfico en el límite de la ventana" que sufren los algoritmos de ventana fija.

Limitación de velocidad en APIs de Express / Node.js

Para las APIs REST tradicionales de Node.js, express-rate-limit es la opción estándar. Esta es una configuración de producción:

js
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import { createClient } from "redis";

const redisClient = createClient({ url: process.env.REDIS_URL });

// Limitador general de API: 100 peticiones cada 15 minutos por IP
export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100,
  standardHeaders: true,     // Devuelve cabeceras RateLimit-*
  legacyHeaders: false,      // Desactiva las cabeceras X-RateLimit-*
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
  }),
  message: { error: "Demasiadas peticiones, inténtalo de nuevo más tarde." },
  keyGenerator: (req) => {
    // Usa X-Forwarded-For detrás de un proxy; si no, la IP
    return req.headers["x-forwarded-for"]?.split(",")[0]
      || req.ip
      || req.connection.remoteAddress;
  },
});

// Limitador estricto para endpoints de autenticación: 5 intentos cada 15 minutos
export const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
  }),
  skipSuccessfulRequests: true, // Solo cuenta los fallos
  message: { error: "Demasiados intentos de inicio de sesión. Inténtalo de nuevo más tarde." },
  keyGenerator: (req) => `${req.ip}:${req.body?.email || "unknown"}`,
});

Patrones clave:

  • skipSuccessfulRequests — solo incrementa el contador en los intentos de autenticación fallidos. Esto evita que los usuarios legítimos queden bloqueados tras un inicio de sesión correcto.
  • Claves compuestas para endpoints de autenticación — limita la velocidad por IP y por email/nombre de usuario a la vez, de modo que un atacante que rote de IP siga topándose con los límites por cuenta.
  • Extracción de X-Forwarded-For — detrás de un proxy inverso (NGINX, Cloudflare, AWS ALB), req.ip siempre es 127.0.0.1. Lee siempre la IP real del cliente desde la cadena de cabeceras.

Aplicar limitadores a las rutas

js
import { apiLimiter, authLimiter } from "./middleware/rateLimit.js";

// Aplicar globalmente a todas las rutas de API
app.use("/api", apiLimiter);

// Aplicar límites más estrictos a los endpoints de autenticación
app.use("/api/auth/login", authLimiter);
app.use("/api/auth/register", authLimiter);

// Aplicar límites diferentes a endpoints costosos
app.use("/api/reports/export", rateLimit({
  windowMs: 60 * 60 * 1000,  // 1 hora
  max: 3,
  store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
}));

Limitación de velocidad en Next.js (App Router)

El middleware de Next.js se ejecuta en cada petición en el edge, lo que lo convierte en el lugar ideal para la limitación de velocidad. El enfoque recomendado usa Upstash Ratelimit (funciona tanto con Redis serverless de Upstash como con Redis autoalojado):

ts
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// Límite de API: 10 peticiones cada 10 segundos por IP
const apiRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "10 s"),
  analytics: true,
  prefix: "ratelimit:api",
});

// Límite de autenticación: 5 peticiones cada 60 segundos por IP+email
const authRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, "60 s"),
  analytics: true,
  prefix: "ratelimit:auth",
});

export async function middleware(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for")?.split(",")[0]
    ?? request.headers.get("x-real-ip")
    ?? "127.0.0.1";

  const { pathname } = request.nextUrl;

  // Aplica límites de velocidad según los patrones de ruta
  if (pathname.startsWith("/api/auth/")) {
    const email = request.nextUrl.searchParams.get("email") || "unknown";
    const identifier = `${ip}:${email}`;
    const { success, limit, remaining, reset } = await authRatelimit.limit(identifier);

    if (!success) {
      return NextResponse.json(
        { error: "Demasiados intentos de autenticación. Inténtalo de nuevo más tarde." },
        {
          status: 429,
          headers: {
            "X-RateLimit-Limit": String(limit),
            "X-RateLimit-Remaining": String(remaining),
            "X-RateLimit-Reset": String(reset),
            "Retry-After": String(Math.ceil((reset - Date.now()) / 1000)),
          },
        }
      );
    }
  }

  if (pathname.startsWith("/api/")) {
    const { success, limit, remaining, reset } = await apiRatelimit.limit(ip);

    if (!success) {
      return NextResponse.json(
        { error: "Demasiadas peticiones." },
        {
          status: 429,
          headers: {
            "X-RateLimit-Limit": String(limit),
            "X-RateLimit-Remaining": String(remaining),
            "X-RateLimit-Reset": String(reset),
          },
        }
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: "/api/:path*",
};

Importante: el middleware de Next.js se ejecuta en el Edge Runtime. Evita importar dependencias pesadas o hacer consultas a la base de datos desde el middleware. La llamada a Redis mediante REST de Upstash es rápida, pero mantén el middleware ligero.

Alternativa con Redis autoalojado

Si autoalojas Redis (o usas una pila de Docker Compose), usa ioredis en su lugar:

ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis as UpstashRedis } from "@upstash/redis";

// Envuelve ioredis para la interfaz del SDK de Upstash
const redisClient = new Redis(process.env.REDIS_URL!);
const redis = UpstashRedis.fromEnv();  // o configura REST manualmente

Alternativamente, omite Upstash por completo y usa express-rate-limit con un manejador de ruta de API personalizado de Next.js (para Route Handlers, no para middleware).

Estrategias de protección contra fuerza bruta

La limitación de velocidad es necesaria pero no suficiente para la protección contra fuerza bruta en autenticación. Combínala con estas capas adicionales:

1. Bloqueo de cuenta tras N fallos

js
// Pseudocódigo — impleméntalo en tu manejador de rutas de autenticación
const FAILURE_THRESHOLD = 5;
const LOCKOUT_DURATION_MS = 15 * 60 * 1000; // 15 minutos

async function checkAccountLockout(email) {
  const key = `lockout:${email}`;
  const attempts = await redis.get(key);
  if (attempts && parseInt(attempts) >= FAILURE_THRESHOLD) {
    const ttl = await redis.ttl(key);
    throw new AccountLockedError(
      `Cuenta bloqueada temporalmente. Inténtalo de nuevo en ${Math.ceil(ttl / 60)} minutos.`
    );
  }
}

async function recordFailedAttempt(email) {
  const key = `lockout:${email}`;
  await redis.incr(key);
  await redis.expire(key, LOCKOUT_DURATION_MS, "NX"); // Establece el TTL solo en el primer incremento
}

2. Respuesta retardada (comparación en tiempo constante)

Nunca reveles por qué falló la autenticación: devuelve siempre el mismo error genérico. Y usa siempre la comparación en tiempo constante para evitar ataques de temporización:

js
import { timingSafeEqual } from "crypto";

function verifyPassword(input, stored) {
  const inputBuf = Buffer.from(input);
  const storedBuf = Buffer.from(stored);
  // La comparación en tiempo constante evita los ataques de canal lateral por temporización
  if (inputBuf.length !== storedBuf.length) {
    return timingSafeEqual(Buffer.from("a"), Buffer.from("b")); // Comparación simulada
  }
  return timingSafeEqual(inputBuf, storedBuf);
}

3. CAPTCHA a partir de un umbral

Muestra un CAPTCHA (Turnstile, reCAPTCHA, hCaptcha o el ALTCHA autoalojado) después de 2-3 intentos fallidos. Esto detiene las herramientas automatizadas sin molestar a los usuarios legítimos:

ts
// Manejador de ruta de API de Next.js
if (failedAttempts >= 2) {
  const { token } = req.body;
  const isValid = await verifyTurnstileToken(token);
  if (!isValid) {
    return res.status(400).json({ error: "Se requiere verificación CAPTCHA." });
  }
}

4. Retardo progresivo

Aumenta gradualmente el tiempo de respuesta a medida que se acumulan los intentos fallidos:

js
const delays = [0, 200, 500, 1000, 2000, 5000]; // ms
const delay = delays[Math.min(failedAttempts, delays.length - 1)];
await new Promise((resolve) => setTimeout(resolve, delay));

Cabeceras de limitación de velocidad: cumplimiento de estándares

Devuelve siempre las cabeceras estándar de limitación de velocidad para que los clientes puedan respetar tus límites de forma programática:

RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Reset: 1719123456
Retry-After: 45

El estándar del IETF (draft-ietf-httpapi-ratelimit-headers) define estas cabeceras. La cabecera Retry-After es la más crítica: indica al cliente exactamente cuándo debe reintentar. Los navegadores y clientes HTTP (como fetch, axios) la respetan automáticamente.

Limitación de velocidad en sistemas distribuidos

Si tu aplicación se ejecuta en varios nodos (pods de Kubernetes, instancias de Lambda, varios servidores), la limitación de velocidad en memoria no funciona: cada nodo tiene su propio contador, y un atacante podría enviar 100 peticiones a cada uno de los 10 pods, sorteando tu límite de 100 por pod.

Usa siempre un almacén centralizado para la limitación de velocidad distribuida:

| Almacén | Ventajas | Inconvenientes | |-------|------|------| | Redis | Rápido, operaciones atómicas de Sorted Set (ZCARD, ZREMRANGEBYSCORE), limpieza automática por TTL | Requiere configurar Redis | | Upstash | Redis serverless vía API REST, sin gestión de pool de conexiones | Latencia de arranque en frío en la primera petición | | DynamoDB | Gestionado, sin infraestructura adicional | Mayor latencia, caducidad basada en TTL, consistencia eventual | | PostgreSQL | Sin servicios adicionales | Lento para alto rendimiento, crecimiento de la tabla |

Redis es la opción estándar para sistemas de producción.

Probar tu limitación de velocidad

Usa artillery o k6 para simular tráfico en ráfagas y verificar que tu limitador responde correctamente:

bash
npm install -D artillery

# artillery.yaml
# config:
#   target: "http://localhost:3000"
#   phases:
#     - duration: 5
#       arrivalRate: 50  # 50 peticiones/segundo durante 5 segundos
# scenarios:
#   - flow:
#       - get:
#           url: "/api/endpoint"

npx artillery run artillery.yaml

Resultado esperado: después de ~2 segundos, todas las peticiones posteriores deben devolver 429 Too Many Requests.

Lista de verificación para el despliegue

  • [ ] Limitación de velocidad aplicada a todos los endpoints de /api/* (no solo a las rutas de autenticación)
  • [ ] Límites distintos para endpoints de autenticación (más estrictos) frente a la API general (más permisivos)
  • [ ] Clave compuesta (ip:email) para la limitación de velocidad en autenticación
  • [ ] Almacén centralizado de Redis para despliegues multi-instancia
  • [ ] Cabeceras estándar de limitación de velocidad (RateLimit-*, Retry-After) en todas las respuestas 429
  • [ ] skipSuccessfulRequests en los limitadores de autenticación
  • [ ] Bloqueo de cuenta con desbloqueo automático tras el periodo de enfriamiento
  • [ ] Comparación en tiempo constante para la verificación de contraseñas
  • [ ] Retardo progresivo de respuesta ante fallos repetidos
  • [ ] Puerta CAPTCHA tras N intentos de inicio de sesión fallidos
  • [ ] Limitación de velocidad en endpoints GraphQL (el análisis de coste por consulta es lo ideal)
  • [ ] Monitorización y alertas sobre eventos de limitación (un pico de 429 = posible ataque)

Resumen

La limitación de velocidad es un control de seguridad fundamental que toda API de producción necesita. Las conclusiones clave:

  1. Empieza simple, evoluciona hacia Redis — los límites en memoria (mediante el MemoryStore por defecto de express-rate-limit) funcionan para el desarrollo en una sola instancia. Pasa a almacenes respaldados por Redis antes de desplegar en producción.

  2. Apila tus defensas — la limitación de velocidad por sí sola no detendrá a un atacante decidido. Combínala con bloqueo de cuenta, puertas CAPTCHA, retardos progresivos y comparaciones en tiempo constante para una protección real contra fuerza bruta.

  3. Usa ventanas deslizantes — las ventanas fijas tienen vulnerabilidades de efecto de borde. Las ventanas deslizantes (o GCRA) proporcionan una limitación de velocidad precisa a escala.

  4. Devuelve siempre las cabeceras — las cabeceras estándar de limitación de velocidad permiten que clientes y CDN (Cloudflare, Fastly) cooperen con tus límites en lugar de luchar contra ellos.

  5. Prueba bajo carga — tu limitador de velocidad solo es tan bueno como su comportamiento bajo tráfico real. Usa artillery, k6 o autocannon para verificar que los límites se aplican correctamente.

La próxima semana: Seguridad de GraphQL — limitación de profundidad, análisis de coste de consultas y cómo asegurar tus endpoints de Apollo/Relay contra ataques de introspección y consultas maliciosas.

Comparte este artículo:TwitterLinkedIn
JS

JS Security Audit

Auditorías dirigidas por un ingeniero de seguridad JavaScript senior con más de 10 años de experiencia.