Rate limiting en aplicaciones web: qué proteger antes de elegir una librería
Cometí el error de agregar rate limiting como si fuera una dependencia de conveniencia — npm install, copiar middleware de un tutorial, pegar el número mágico de 100 requests por minuto y seguir con el sprint. Lo hice porque "seguridad" estaba en el backlog y quería tildar el ítem. El resultado fue predecible: el middleware existía, pero no protegía nada en particular. Y la primera vez que revisé los logs con criterio, me di cuenta de que no sabía qué habría pasado si alguien hubiera abusado del endpoint de login.
Lo cuento porque ese patrón es exactamente el que veo circular en tutoriales de Next.js: instalar una librería, enrollarla como middleware global y llamarle "seguridad". No es seguridad. Es vibra de seguridad.
Mi tesis es concreta: rate limiting no es una dependencia; es una política de abuso. Y una política sin definición es una regla sin sujeto.
Qué es rate limiting en aplicaciones web con Next.js — y qué no es
Rate limiting es un mecanismo para rechazar o demorar solicitudes que superan un umbral definido en una ventana de tiempo. Eso es todo lo que es a nivel técnico. El valor real no está en el umbral — está en la decisión que llevó a ese umbral.
OWASP, en su Authentication Cheat Sheet, recomienda controles defensivos específicos alrededor de autenticación: bloqueo temporal de cuenta tras intentos fallidos, throttling progresivo y respuestas uniformes para no revelar si el usuario existe. Lo que OWASP no dice es "instalá X librería y configurá 100 req/min en todas las rutas". Eso es una interpretación, no una prescripción.
La diferencia importa: la guía de OWASP te da el qué proteger (autenticación, recuperación de contraseña, endpoints que exponen estado de cuenta). La implementación concreta depende de tu stack, tu tráfico y el costo que estás dispuesto a asumir cuando bloqueás a alguien legítimo.
En Next.js App Router, el lugar natural para interceptar es el Middleware (middleware.ts), que corre en el edge antes de que el request llegue a la ruta — lo analicé en detalle en este post sobre patrones de autorización en Next.js 16 Middleware. Pero la capa de ejecución no reemplaza la política; solo la aplica.
El error clásico: copiar el middleware antes de definir el activo
El tutorial típico empieza así:
// middleware.ts — ejemplo de lo que NO hacer primero
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, "1 m"), // ¿por qué 100? ¿por qué 1 minuto?
});
export async function middleware(request: NextRequest) {
const ip = request.ip ?? "127.0.0.1";
const { success } = await ratelimit.limit(ip);
if (!success) return new NextResponse("Too Many Requests", { status: 429 });
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next|favicon.ico).*)"], // aplica a TODO — ¿seguro?
};El código funciona. El problema es la cadena de preguntas sin responder:
- ¿Por qué 100 requests por minuto? ¿Basado en qué comportamiento legítimo medido?
- ¿Aplicarlo a todas las rutas tiene sentido? Una imagen estática y un endpoint de login no tienen el mismo perfil de abuso.
- ¿Qué le pasa a un usuario legítimo detrás de un proxy corporativo o una red universitaria donde muchos comparten la misma IP?
- ¿Hay un log del 429 que permita distinguir ataque real de falso positivo?
Ninguna de esas preguntas la resuelve la librería. Las resolvés vos antes de tocar el código.
La matriz de decisión: cuatro preguntas antes de escribir una línea
Antes de elegir algoritmo, librería o umbral, estas cuatro preguntas definen si el rate limiting que vas a implementar tiene sentido:
1. ¿Qué activo protegés?
No "la app". Algo concreto:
| Activo | Por qué importa limitarlo |
|---|---|
| Endpoint de login | Credential stuffing, fuerza bruta |
| Endpoint de recuperación de contraseña | Enumeración de cuentas, spam de emails |
| API de envío de formularios | Spam, flood de notificaciones |
| Endpoint de búsqueda costosa | Abuso de recursos de cómputo |
| Rutas estáticas / assets | Probablemente no — dejalo a CDN |
OWASP señala explícitamente los dos primeros. Los otros son decisiones de producto que vos tenés que tomar.
2. ¿Qué abuso esperás?
El abuso que querés limitar determina el algoritmo correcto:
- Credential stuffing de alta velocidad: sliding window con umbral bajo por IP en el endpoint de auth.
- Scraping lento y distribuido: IP sola no alcanza — necesitás fingerprinting o tokens de sesión.
- Spam de formularios automatizado: CAPTCHA primero, rate limiting como segunda capa.
- Picos de tráfico legítimo (lanzamiento, viral): rate limiting estricto puede hacerte daño — considerá queueing o backpressure primero.
Si no sabés qué abuso esperás, el umbral que ponés es arbitrario. Y un umbral arbitrario tiene la misma probabilidad de molestar a usuarios reales que de detener a alguien con intención maliciosa.
3. ¿Cuánto te cuesta un falso positivo?
Este es el costo que los tutoriales omiten. Un 429 a un usuario legítimo tiene consecuencias reales que dependen del contexto:
- En un SaaS con clientes pagos: pérdida de confianza, churn, ticket de soporte.
- En una app pública con usuario anónimo: frustración, abandono.
- En una API interna: puede romper un flujo crítico silenciosamente.
El costo del falso positivo define cuánto podés apretar el umbral. Si el costo es alto, necesitás un umbral más permisivo y mejores señales de abuso (user-agent, comportamiento, tokens) en lugar de solo IP.
4. ¿Cómo observás que está funcionando?
Si no tenés respuesta para esto, lo que implementaste es seguridad decorativa. Un rate limiter sin observabilidad no te dice si está bloqueando abuso real o usuarios legítimos, si el umbral es demasiado agresivo o demasiado permisivo, ni si alguien está evadiendo el control con otra técnica.
El mínimo útil es loggear cada 429 con:
- Timestamp
- IP (o identificador que uses como clave)
- Ruta afectada
- Contexto de autenticación si está disponible (usuario autenticado vs. anónimo)
// middleware.ts — versión con observabilidad mínima
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// Ejemplo reproducible con almacenamiento en memoria (solo para desarrollo o edge con estado local)
// En producción real necesitás Redis u otro store distribuido
const intentos = new Map<string, { count: number; reset: number }>();
const LIMITE = 10; // solo para el endpoint de login
const VENTANA_MS = 60_000; // 1 minuto
export async function middleware(request: NextRequest) {
// Solo aplicamos a la ruta de login — activo definido, no global
if (!request.nextUrl.pathname.startsWith("/api/auth/login")) {
return NextResponse.next();
}
const ip = request.headers.get("x-forwarded-for")?.split(",")[0] ?? "desconocida";
const ahora = Date.now();
const registro = intentos.get(ip);
if (!registro || ahora > registro.reset) {
intentos.set(ip, { count: 1, reset: ahora + VENTANA_MS });
return NextResponse.next();
}
registro.count++;
if (registro.count > LIMITE) {
// Log observable: en producción esto iría a tu sistema de logs
console.warn(JSON.stringify({
evento: "rate_limit_excedido",
ip,
ruta: request.nextUrl.pathname,
intentos: registro.count,
timestamp: new Date().toISOString(),
}));
return new NextResponse(
JSON.stringify({ error: "Demasiados intentos. Esperá un momento." }),
{
status: 429,
headers: {
"Content-Type": "application/json",
"Retry-After": "60",
},
}
);
}
return NextResponse.next();
}
export const config = {
// Solo interceptamos la ruta que definimos como activo
matcher: ["/api/auth/login"],
};⚠️ Este ejemplo usa
Mapen memoria, que no persiste entre instancias del edge runtime ni sobrevive un redeploy. Para producción sobre Railway u otro entorno distribuido, necesitás un store externo como Redis (Upstash, Redis Cloud). El ejemplo sirve como patrón de decisión, no como receta de producción directa.
Dónde se equivoca la gente: tres patrones que parecen bien pero no lo son
Patrón 1: Rate limiting global como sustituto de seguridad de endpoint
Un middleware que limita todas las rutas a 100 req/min no protege el endpoint de login mejor que uno sin rate limiting, si el umbral está por encima del volumen de un ataque de fuerza bruta típico. El atacante simplemente respeta el límite y sigue. Lo que realmente ayuda es un umbral bajo y específico en el activo correcto — más en la línea de lo que recomienda OWASP para autenticación.
Patrón 2: IP como única dimensión de clave
Un usuario detrás de CGNAT (IPv4 compartida entre miles) tiene la misma IP que otro. En contextos corporativos o educativos, limitarlos a todos juntos puede bloquear a decenas de personas legítimas por el comportamiento de una sola. Si el activo que protegés es accedido principalmente por usuarios autenticados, la clave debería ser el identificador de usuario o sesión, no la IP.
Patrón 3: Olvidar el header Retry-After
RFC 6585 define que una respuesta 429 debería incluir el header Retry-After indicando cuánto debe esperar el cliente. Sin ese header, clientes automáticos (SDKs, apps móviles, integraciones) van a reintentar inmediatamente y agravar el problema que intentabas limitar. Es un detalle pequeño con consecuencias concretas.
Límites de esta guía: qué no podés concluir sin datos propios
Hay cosas que no voy a afirmar acá porque dependen de contexto que no tengo:
- Qué umbral usar: no existe un número correcto universal. El 10 req/min del ejemplo de login es ilustrativo. El número real lo define el comportamiento legítimo medido en producción — o una decisión conservadora inicial con capacidad de ajuste.
- Si Upstash, Redis Cloud u otro store es mejor: dependen de la latencia desde donde corrés el edge, el costo por operación y la complejidad operativa que estás dispuesto a mantener.
- Si el rate limiting resuelve scraping lento y distribuido: probablemente no, al menos no solo. Ese escenario requiere otras señales. Afirmar lo contrario sin datos sería venderle una solución incompleta a alguien con un problema real.
Antes de asumir que el rate limiting funciona, necesitás logs reales. Sin ellos, el control existe en papel pero no en práctica.
FAQ: rate limiting en aplicaciones web con Next.js
¿Dónde implemento rate limiting en Next.js App Router — en Middleware o en la Route Handler?
Depende del activo. Si querés actuar antes de que el request llegue a la lógica de la ruta (y antes de costos de compute), el Middleware es el lugar correcto. Si necesitás contexto de autenticación completo o lógica de negocio para decidir el límite, la Route Handler tiene más información. En la práctica, los dos capas pueden coexistir: Middleware para límites gruesos por IP, Route Handler para límites finos por usuario autenticado.
¿Puedo usar un Map en memoria como store para el rate limiter?
Solo en desarrollo o como demostración de patrón. Un Map en memoria no se comparte entre instancias (Next.js puede tener múltiples workers) y se reinicia con cada redeploy. Para un ambiente distribuido como Railway o Vercel, necesitás un store externo — Redis es la opción más común y documentada.
¿El rate limiting reemplaza CAPTCHA en el formulario de login?
No. Son controles diferentes. CAPTCHA apunta a distinguir humanos de bots en tiempo real. Rate limiting apunta a limitar el volumen de intentos independientemente de si son humanos o bots. OWASP sugiere los dos como capas complementarias, no como alternativas.
¿Qué pasa si un usuario legítimo toca el límite por error?
Debería recibir un 429 con un Retry-After claro y un mensaje entendible. Si eso pasa frecuentemente, el umbral está mal calibrado para el tráfico legítimo — eso es una señal para revisar el número, no para eliminar el control.
¿Rate limiting alcanza para proteger una API pública de scraping masivo?
Probablemente no, si el scraping es distribuido (muchas IPs distintas con volumen bajo cada una). El rate limiting por IP solo funciona bien contra fuentes de alta frecuencia concentradas. Scraping distribuido requiere otras señales: fingerprinting de user-agent, análisis de patrones de comportamiento o autenticación obligatoria con tokens.
¿Conviene aplicar rate limiting a rutas de assets estáticos?
Generalmente no — eso es trabajo para un CDN o para la capa de infraestructura. Aplicar rate limiting a /favicon.ico o a imágenes desde el Middleware de Next.js agrega latencia sin beneficio defensivo real. Si el tráfico de assets es el problema, el control adecuado está en la capa de red, no en la aplicación.
Cierre: la librería no decide la política, vos sí
Hay una razón por la que el rate limiting mal configurado es peor que ninguno: da la sensación de que el activo está protegido cuando no lo está, o protege algo que no importa mientras deja desprotegido lo que sí importa.
Mi postura es esta: antes de instalar cualquier librería, respondé las cuatro preguntas de la matriz. Si no podés responder "qué activo protejo" y "qué abuso espero" con algo más específico que "la app" y "gente maliciosa", no estás lista para implementar la política. Estás para copiar un tutorial.
El próximo paso concreto: revisá tus rutas de autenticación primero. Son las que OWASP marca con más evidencia de necesitar control. Definí un umbral conservador, agregá observabilidad mínima (loggear los 429) y revisá esos logs en las primeras 48 horas. Ahí vas a tener datos reales para calibrar.
Si querés entender mejor cómo el Middleware de Next.js ejecuta estos controles y qué pasa con los race conditions cuando escala, el post sobre patrones de autorización en Next.js 16 Middleware es el siguiente paso lógico. Y si la seguridad de endpoints de backend es parte del contexto, vale cruzar con lo que cubrí sobre qué exponer y qué ocultar en Spring Boot Actuator.
Fuente original:
- OWASP Authentication Cheat Sheet — controles defensivos alrededor de autenticación y abuso: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
Artículos Relacionados
Next.js 16 Middleware: patrones de autorización que escalan y los que generan race conditions
Probé 4 patrones de autorización en Next.js 16 Middleware con edge runtime. Uno genera race conditions silenciosas, otro te da latencia inesperada, y uno solo escala sin compromisos. Acá el análisis honesto de cada tradeoff.
Prisma 5 → Prisma 6: los breaking changes que encontré en mi schema real y cómo los resolví sin romper producción
Prisma 6 mejora ergonomía y performance, pero hay tres cambios de comportamiento que no gritan en el compilador y sí aparecen en runtime si no revisás tus queries relacionales. Guía práctica con checklist.
tsgo: qué cambia en el compilador TypeScript reescrito en Go y qué significa para proyectos reales
tsgo es real y el salto de performance es verificable, pero la beta tiene límites documentados que la mayoría ignora. Acá está el criterio concreto para saber si vale explorarlo hoy o si conviene esperar la stable.
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.