Rate limiting en aplicaciones web: qué proteger antes de elegir una librería
Estuve meses convencido de que rate limiting era cuestión de agregar un middleware, configurar un número de requests por minuto y seguir. Me alcanzó con ver un endpoint de login sin ningún control —en un proyecto de referencia que usé para estudiar arquitectura— para entender que el problema no era la librería que faltaba. Era que nadie había definido qué se estaba protegiendo, de qué tipo de abuso y a qué costo.
No lo cuento para quedar mal, lo cuento porque probablemente vos también vas a copiar ese snippet de upstash/ratelimit o express-rate-limit antes de hacerte esa pregunta. Y si lo hacés, vas a tener rate limiting que funciona en el papel y falla exactamente donde más importa.
Mi tesis: rate limiting no es una dependencia. Es una política de abuso. Y una política sin contexto es decoración.
Rate limiting en aplicaciones web con Next.js: el problema real
La mayoría de los tutoriales sobre rate limiting en Next.js arrancan desde la librería. Te muestran cómo configurar Upstash Redis, cuántos tokens poner en el bucket y cómo devolver un 429. Todo eso está bien. El problema es que esa secuencia saltea cuatro preguntas que deberían ir primero:
- ¿Qué activo estás protegiendo? ¿Un endpoint de login? ¿Una API pública sin autenticación? ¿Un formulario de contacto? ¿Una ruta que dispara un email? No todos merecen la misma política.
- ¿Qué tipo de abuso esperás? Credential stuffing, scraping, spam de formulario y DDoS son cuatro problemas distintos. Rate limiting resuelve algunos, no todos.
- ¿Cuánto te cuesta un falso positivo? Bloquear a un usuario legítimo en el checkout de un e-commerce tiene un costo diferente al de bloquear un bot en una API de consulta pública.
- ¿Cómo vas a saber que está funcionando? Sin logs, sin métricas, sin alertas, el middleware actúa en silencio y no sabés si está bloqueando lo que tiene que bloquear o lo que no debería.
Estos cuatro puntos no son filosóficos. Son la diferencia entre un control que protege y uno que da una sensación falsa de seguridad.
Qué dice OWASP y qué no dice
La OWASP Authentication Cheat Sheet es la referencia pública más citada cuando se habla de controles defensivos en autenticación y abuso. Vale la pena leerla directo en lugar de confiar en el resumen de alguien más.
Lo que OWASP dice explícitamente en ese documento:
- Implementar controles de límite de intentos fallidos de login para mitigar ataques de fuerza bruta.
- Considerar lockout temporal, CAPTCHA o notificación al usuario como respuestas posibles.
- El lockout agresivo puede usarse como vector de denegación de servicio contra usuarios legítimos (es decir, el falso positivo tiene costo de seguridad, no solo de UX).
Lo que OWASP no dice en ese documento:
- Cuántas requests por minuto es el número correcto. No existe ese número fuera de contexto.
- Que rate limiting por IP sea suficiente. Un atacante con infraestructura distribuida rota IPs trivialmente.
- Que todas las rutas de una aplicación merezcan el mismo control.
Esto importa porque mucha gente cita "OWASP recomienda rate limiting" como si fuera una receta universal. No lo es. Es un control entre varios, con precondiciones y trade-offs explícitos.
Si ya leíste mi post sobre qué exponer y qué ocultar en Spring Boot Actuator, reconocés el patrón: el problema no es activar o desactivar una feature. Es entender qué decisión tomás y por qué.
Dónde se equivoca la receta común
La receta típica en Next.js App Router se ve así: instalás @upstash/ratelimit, wrapeás el handler de la Route Handler o el Server Action, configurás 10 requests por minuto por IP y devolvés 429 si se supera. Funciona. Y acá está el problema: funciona para el caso que no importa.
Costo oculto 1: la granularidad por IP es débil donde más duele.
Los endpoints más sensibles —login, recuperación de contraseña, registro— son exactamente los que un atacante con recursos va a atacar desde múltiples IPs. Rate limiting por IP frena el abuso amateur. No frena credential stuffing bien armado. Para eso necesitás combinar IP + User-Agent fingerprinting, o directamente CAPTCHA en el flujo.
Costo oculto 2: el 429 sin logging es un control ciego.
Si el middleware bloquea y no emitís un log estructurado con al minimum { ip, route, timestamp, reason }, no tenés forma de distinguir un ataque real de un usuario detrás de un NAT corporativo que comparte IP con cien colegas. Ese usuario legítimo va a recibir un 429 y no va a entender por qué.
Costo oculto 3: la configuración global aplana la política.
Un límite global de 100 requests por minuto puede ser generoso para un endpoint de login y absurdamente restrictivo para una API de búsqueda pública. Cuando todo usa la misma configuración, la política no tiene semántica real: es un número arbitrario que nadie justificó.
Contraejemplo concreto: un formulario de contacto que dispara un email tiene un costo real por request (el envío del email). Ahí rate limiting por IP + por sesión tiene sentido, y el umbral debería ser bajo. Un endpoint de lectura pública que sirve datos estáticos ya cacheados tiene un perfil de abuso completamente diferente. Modelarlos igual es impreciso.
En el mismo sentido en que hablé de caching en Next.js App Router: no existe una configuración que sirva para todo. Cada ruta tiene un contrato de comportamiento.
Checklist de decisión: antes de copiar el middleware
Este checklist no reemplaza el análisis de cada sistema. Es un punto de partida para estructurar la conversación antes de abrir la documentación de la librería.
1. Definí el activo
| Tipo de ruta | Sensibilidad | Control mínimo recomendado |
|---|---|---|
| Login / autenticación | Alta | Rate limit por IP + por usuario si existe; considerar CAPTCHA en reintentos |
| Recuperación de contraseña | Alta | Limit por IP; notificación al usuario; no revelar si el email existe |
| Formulario con efecto colateral (email, pago) | Media-Alta | Rate limit por IP + sesión; log de cada bloqueo |
| API pública de lectura sin auth | Media | Rate limit global por IP; considerar si el caching ya mitiga el riesgo |
| API autenticada | Baja-Media | Rate limit por token/usuario; política diferenciada por plan si aplica |
| Rutas estáticas o de health | No aplica | Sin rate limit; el costo de falso positivo es máximo |
Criterios prudentes basados en patrones documentados. El umbral numérico depende del volumen real de cada sistema.
2. Definí el abuso esperado
- Fuerza bruta / credential stuffing: control en el endpoint de autenticación, no en toda la app.
- Spam de formulario: rate limit por IP + honeypot + CAPTCHA si el volumen lo justifica.
- Scraping: rate limit por IP + headers de User-Agent; considerar si el contenido debería ser público de todas formas.
- DDoS de capa 7: rate limiting en la app no es suficiente; ese problema vive en la capa de infraestructura (CDN, WAF, Railway regions).
3. Medí el costo del falso positivo
Antes de configurar un umbral, contestá: ¿qué pasa si un usuario legítimo lo supera? Si la respuesta es "pierde una sesión que puede recuperar fácil", el umbral puede ser más agresivo. Si la respuesta es "pierde una compra o no puede entrar a una cuenta activa", el umbral necesita más tolerancia o un mecanismo de desafío progresivo en lugar de bloqueo directo.
4. Definí observabilidad desde el día uno
Un snippet mínimo de lo que deberías loguear en cada bloqueo:
// app/api/login/route.ts — ejemplo reproducible, no código de producción
import { NextRequest, NextResponse } from 'next/server'
// El chequeo de rate limit va ANTES de cualquier lógica de negocio
export async function POST(req: NextRequest) {
const ip = req.headers.get('x-forwarded-for') ?? 'unknown'
const limitExceeded = await checkRateLimit(ip) // vos implementación
if (limitExceeded) {
// Loguear SIEMPRE: IP, ruta, timestamp, motivo
console.warn(JSON.stringify({
event: 'rate_limit_exceeded',
ip,
route: '/api/login',
timestamp: new Date().toISOString(),
}))
return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
}
// Lógica de autenticación acá
}
Sin ese log, el middleware es una caja negra. No sabés si bloqueó diez bots o diez usuarios reales. El tema de observabilidad lo traté también en el post de Docker healthchecks: medir bien es parte del control, no un extra opcional.
Límites honestos: qué no podés concluir sin datos propios
Esto lo digo explícitamente porque mucho contenido sobre rate limiting lo omite:
- No podés saber el umbral correcto sin logs reales de tráfico. El número 10, 100 o 1000 requests por minuto no tiene sentido fuera de contexto. Si no tenés datos de uso real, empezá con un umbral conservador, loguéalo y ajustá.
- Rate limiting por IP no es equivalente a rate limiting por usuario. Detrás de un NAT corporativo puede haber decenas de usuarios en la misma IP. Una política agresiva por IP en ese contexto genera falsos positivos sistemáticos.
- No podés asumir que el 429 llegó al atacante correcto. Sin logs, no sabés a quién bloqueaste. Con logs, tenés evidencia para ajustar.
- Rate limiting no reemplaza autenticación, autorización ni validación de inputs. Es un control de última línea frente a abuso volumétrico, no una capa de seguridad general. La misma lógica aplica a firma digital: cada control tiene su dominio, no hay uno que cubra todo.
FAQ: preguntas frecuentes sobre rate limiting en Next.js
¿Qué librería de rate limiting recomendás para Next.js?
@upstash/ratelimit es la opción más documentada para Next.js con Redis en el edge. express-rate-limit funciona si el backend es Node.js puro. Pero la librería es el último paso, no el primero. Si no definiste qué activo protegés y qué abuso esperás, cualquier librería te da una falsa sensación de control.
¿Rate limiting en middleware de Next.js o en cada Route Handler?
Depende de la granularidad que necesitás. El middleware de Next.js corre antes de que la request llegue a la ruta: es útil para protección global o por prefijo. Los Route Handlers permiten políticas distintas por endpoint. Lo más práctico suele ser: política base en middleware, política específica en los endpoints sensibles (login, recuperación de contraseña, formularios con efectos).
¿IP es suficiente como identificador para rate limiting?
Para abuso amateur, sí. Para ataques distribuidos, no. Si el activo que protegés es crítico —login, pagos, endpoints que disparan emails—, combiná IP con identificador de sesión o usuario cuando exista, y considerá CAPTCHA como desafío progresivo en lugar de bloqueo directo.
¿Cómo manejo rate limiting en Railway sin Redis?
Railway permite agregar Redis como servicio interno. Si no querés esa dependencia, podés usar un store en memoria para prototipos —pero tenés que aceptar que el límite no se comparte entre instancias. En producción con múltiples réplicas, un store compartido es necesario para que la política sea consistente.
¿El rate limiting de la capa de infraestructura (CDN, WAF) reemplaza el del código?
No, son complementarios. La capa de infraestructura protege contra volumen bruto y DDoS de capa 7. El rate limiting en la aplicación permite políticas semánticas: por usuario, por tipo de acción, por costo de la operación. Los dos juntos son más robustos que cualquiera de los dos solos.
¿Qué hago si el rate limiting bloquea usuarios legítimos?
Primero, loguéalo. Segundo, revisá si el problema es el umbral (muy bajo) o el identificador (IP en lugar de usuario). Tercero, considerá un mecanismo de desafío progresivo: en lugar de bloquear directo, pedí confirmación, CAPTCHA o una espera corta. El bloqueo duro debería ser el último recurso, no la respuesta por defecto.
Conclusión: la política antes que la dependencia
La escena con la que arranqué —un endpoint sin ningún control de abuso en un proyecto de referencia— no era un problema de librería faltante. Era un problema de diseño: nadie había definido qué proteger, de qué y a qué costo.
Ese orden importa. Si primero elegís la librería y después te preguntás qué configurar, terminás con un middleware que actúa pero no tiene política. Actúa en silencio, bloquea sin criterio documentado y no te dice nada cuando falla.
Lo que me parece honesto recomendar:
- Listá los activos que merecen protección —no toda la app, los endpoints con efectos colaterales reales.
- Definí el tipo de abuso antes de elegir el control. Son problemas distintos.
- Calculá el costo del falso positivo para cada activo antes de configurar el umbral.
- Loguéalo desde el día uno. Sin logs, el control es decoración.
- Usá la librería que encaje con lo anterior, no al revés.
Rate limiting bien aplicado es un control defensivo real. Mal aplicado es un número en una configuración que nadie va a revisar hasta que un usuario legítimo llame por teléfono preguntando por qué no puede entrar.
La pregunta que te dejo es concreta: ¿sabés hoy cuántos 429 devolvió la app en las últimas 24 horas y a quién?
Fuente original:
- OWASP Authentication Cheat Sheet — controles defensivos alrededor de autenticación y abuso: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)