Activaste la limitación de tasa, tus gráficas se calmaron y luego Soporte comenzó a recibir avisos: “los clientes no pueden iniciar sesión”. El tráfico de bots desapareció. También los humanos. Bienvenido a la trampa clásica de Nginx: activar la limitación es sencillo y afinarla para que encaje con el comportamiento real de usuarios es sorprendentemente difícil.
Esta es una guía orientada a producción para sistemas Ubuntu 24.04 que ejecutan Nginx. Elegiremos claves sensatas, dimensionaremos zonas de memoria compartida, gestionaremos ráfagas sin premiar a los abusadores y diagnosticaremos 429 sin conjeturas. Saldrás con configuraciones que puedes defender en una revisión de cambios y con un plan de acción que puedes ejecutar a las 02:00 con un ojo abierto.
Qué hace realmente la limitación de tasa de Nginx (y qué no hace)
Nginx tiene dos reguladores principales que la gente agrupa como “limitación de tasa”:
limit_req: limita la tasa de peticiones por clave (peticiones por segundo/minuto). Piensa “¿qué tan rápido está pidiendo este cliente?”limit_conn: limita el número de conexiones simultáneas por clave. Piensa “¿cuántos sockets está manteniendo abiertos este cliente?”
limit_req es lo que usas para protección contra fuerza bruta, equidad de API y mitigar spray básico de DDoS. limit_conn es lo que usas para comportamientos tipo slowloris, clientes excesivamente habladores o un error de CDN/monitoring que abre demasiadas conexiones.
Lo que ninguno de los dos hace: no distinguen “usuarios reales” de “usuarios falsos”. Hacen cumplir una política. Si tu clave de política es incorrecta o tus umbrales ignoran el comportamiento web moderno (peticiones paralelas, reintentos, redes móviles), bloquearás a las mismas personas que te pagan el sueldo.
También: la limitación no es un WAF, no es detección de bots y no reemplaza el control de autenticación. Es un disyuntor para patrones de volumen de peticiones.
Algunos hechos e historia que facilitan el ajuste
Esto no es trivia por el gusto de la trivia. Son las razones por las que una regla “5r/s por IP” funciona en un entorno y explota en otro.
- El keepalive de HTTP/1.1 cambió el significado de “conexiones”. En HTTP temprano se abrían muchas conexiones cortas; keepalive hizo que una sola conexión lleve muchas peticiones. Por eso
limit_connpuede ser inútil para inundaciones de peticiones y devastador para WebSockets. - La multiplexación de HTTP/2 cambió el significado de “paralelo”. Los navegadores pueden ejecutar muchas streams concurrentes en una sola conexión TCP. Un cliente puede generar ráfagas sin abrir más sockets, así que los controles por tasa de peticiones importan más que el conteo de conexiones.
- CDNs y NAT de operador colapsaron usuarios en IPs compartidas. Limitar por IP puede castigar oficinas enteras, escuelas, hoteles y pools de egress de operadores móviles. Esto no es un caso marginal teórico; es martes.
- La limitación de Nginx usa una zona de memoria compartida. Es rápida porque está en memoria y es compartida entre workers. También es acotada; cuando se llena, expulsa entradas antiguas. La expulsión cambia el comportamiento bajo carga.
- El modelo “balde con fuga” es antiguo, pero sigue funcionando.
limit_reqes un limitador estilo bucket/token. Es predecible y barato comparado con cheques externos por petición. - 429 Too Many Requests es un contrato moderno. No es solo “bloqueado”. Muchos clientes reintentarán; algunos retrocederán; otros golpearán más fuerte. Devolver 429 o 503 cambia la forma del tráfico.
- Los reintentos se volvieron normales. Clientes móviles, mallas de servicios y librerías reintentan agresivamente. Una “leve latencia” puede convertirse en una ráfaga de reintentos que dispara tu limitador y empeora el día.
- Los atacantes se adaptaron a límites ingenuos hace años. Las botnets rotan IPs; los atacantes de credenciales distribuyen intentos entre muchas fuentes; los scrapers usan proxies residenciales. Un límite global por IP es en su mayoría un tope de velocidad.
Una cita que debería estar en cada wiki de on-call: La esperanza no es una estrategia.
— James Cameron. Es corta, directa y correcta.
Principios: no limites “usuarios”, limita comportamientos
Si intentas proteger “el sitio web” con un solo limitador, o serás demasiado estricto para los humanos o demasiado indulgente con el abuso. En su lugar:
- Protege acciones costosas más que las baratas. Intentos de login, restablecimientos de contraseña, endpoints de búsqueda y páginas dinámicas reciben límites más estrictos. Los assets estáticos pueden ser básicamente ilimitados (o enviados a una CDN).
- Prefiere claves de identidad sobre claves de red cuando puedas. Limita por API key, ID de usuario o cookie de sesión para tráfico autenticado. La IP es el último recurso para tráfico “desconocido”.
- Permite ráfagas, pero limita el abuso sostenido. Los humanos hacen clic, las apps precargan, los navegadores abren múltiples conexiones y el JS hace peticiones en segundo plano. Una pequeña tolerancia a ráfagas previene falsos positivos.
- Falla “de forma educada” para clientes que reintentarán. Si devuelves 429, incluye
Retry-Aftercuando puedas. Estás modelando el tráfico, no cerrando la puerta de golpe. - Hazlo observable. Si no puedes responder “quién fue limitado, en qué endpoint, con qué clave y si era esperado”, estás volando a ciegas.
Chiste 1/2: La limitación de tasa es como un portero de discoteca: los buenos evitan peleas, los malos echan al contable porque “parecía sospechoso”.
Elegir la clave correcta: IP, usuario, token o algo más inteligente
La clave es el corazón de tu limitador. Define “quién” está siendo contado. La mayor parte del dolor surge al elegir una clave que no refleja la realidad de tu tráfico.
Opción A: $binary_remote_addr (basado en IP)
Este es el patrón por defecto porque es simple y no requiere cooperación de la aplicación.
- Pros: Funciona para tráfico anónimo; barato; sin cambios en la aplicación.
- Contras: NAT y proxies colapsan múltiples usuarios en un bucket; IPs rotativas lo evaden; detrás de LBs puedes estar limitando al propio LB si la IP real no está configurada.
Úsalo para: intentos de fuerza bruta en endpoints públicos, protección base para áreas no autenticadas, mitigación burda durante un incidente.
Opción B: identidad autenticada (cookie, reclamo JWT, API key)
Si tienes un header de API key como X-API-Key, o un reclamo JWT que puedas mapear a una variable, puedes limitar por cliente en lugar de por gateway NAT.
- Pros: Justo; resistente a problemas de NAT; se alinea con límites de facturación/abuso.
- Contras: Requiere parseo fiable; los paths no autenticados aún necesitan fallback; keys filtradas pueden ser abusadas.
Opción C: claves compuestas (IP + clase de endpoint, o IP + UA)
Las claves compuestas son útiles cuando no puedes confiar en la identidad y aun quieres separar comportamientos.
Ejemplo: limitar intentos de login por IP, pero también aplicar un techo global bajo para una sola IP que golpea muchos usernames. Nginx no hará correlación entre claves por ti, pero puedes definir zonas distintas por ubicación y elección de clave.
Qué recomiendo en producción
Usa dos capas:
- Capa anónima: límite por IP para endpoints sensibles (login, restablecimiento de contraseña, búsqueda). Ráfaga moderada. Tasa sostenida estricta.
- Capa autenticada: límite por token/API key para APIs. Ráfaga mayor. Tasa sostenida más alta. Límites separados por plan si puedes.
Y si estás detrás de un proxy inverso, arregla la IP real primero. Si no lo haces, estarás limitando un balanceador y llamándolo “seguridad”.
Dimensionar zonas de memoria compartida y entender la expulsión
Nginx almacena el estado del limitador en una zona de memoria compartida definida por limit_req_zone. Esa zona tiene un tamaño fijo. Cuando se llena, las entradas antiguas se expulsan. La expulsión provoca dos tipos de comportamiento extraño:
- Sub-limitación: clientes abusivos rotan suficientes claves únicas para expulsarse a sí mismos de la zona, “olvidando” su historial.
- Sobrelimitación de inocentes: menos común, pero cuando una zona hace thrashing, puedes ver comportamiento inestable donde clientes a veces pasan y a veces reciben 429, dependiendo de si su entrada está residente.
La documentación de Nginx suele sugerir memoria aproximada por entrada, pero en la práctica dimensionas según claves únicas que esperas durante picos. Para limitación por IP en un sitio público, las claves únicas pueden ser sorprendentemente altas.
Heurística práctica de dimensionamiento
Comienza con:
- 10m para sitios pequeños o endpoints de un solo propósito
- 50m–200m para edges públicos de alto tráfico
Luego mide. Si ejecutas múltiples zonas (recomendado), cada zona necesita su propia memoria.
Ráfaga, nodelay y por qué lo “suave” no siempre es benévolo
La mayoría de los falsos positivos ocurren por ráfagas. Los navegadores reales y las apps móviles no hacen una petición por segundo como robots educados. Hacen esto:
- Cargan HTML
- Inmediatamente solicitan CSS, JS, imágenes, fuentes
- Ejecutan llamadas API (a veces varias) cuando carga el JS
- Reintentan rápidamente una petición fallida
limit_req soporta burst y nodelay:
burst=Npermite N peticiones excesivas en cola (o retrasadas), suavizando un pico.nodelayhace que las ráfagas pasen inmediatamente hasta el límite burst (sin retraso); más allá de eso, las peticiones son rechazadas.
El intercambio:
- burst sin nodelay: más amable con los backends, pero los usuarios experimentan latencia y pueden reintentar, lo que puede amplificar la carga.
- burst con nodelay: mejor UX para pequeñas ráfagas, pero puede entregar un golpe más fuerte a tu upstream durante ataques.
Para endpoints de login, a menudo prefiero sin nodelay (retrasar a los abusadores) pero con una ráfaga pequeña para dobles clics legítimos. Para endpoints de API donde los clientes expiran y reintentan, a menudo prefiero nodelay con una ráfaga moderada, de modo que microráfagas “normales” no se conviertan en latencia de larga cola.
Límites diferentes para distintos endpoints (login no es CSS)
Deja de aplicar un limitador único a / y darlo por hecho. Quieres políticas por clase:
- Login / auth: estricto por IP, muy estricto por nombre de usuario si la app lo soporta (Nginx por sí solo no puede, pero puedes clavear por un parámetro de login solo con módulos extra; a menudo lo haces en la app). Añade exenciones por sesión con cuidado.
- Restablecimiento de contraseña / OTP: estricto. El abuso aquí cuesta dinero y reputación.
- Búsqueda / consultas costosas: estricto por IP o por token, porque a los bots les encantan los endpoints de búsqueda.
- API general: por API key/cliente. Diferentes niveles si puedes.
- Assets estáticos: normalmente sin
limit_req; usa caché CDN o déjalos fluir. Si debes, usa un límite muy alto y enfócate en controles de conexión. - WebSocket / SSE: no uses limitación por tasa de petición después de la upgrade; considera límites de conexión keyeados por IP o token.
Detrás de un balanceador: IP real, límites de confianza y suplantación
En Ubuntu 24.04, Nginx frecuentemente está detrás de un balanceador de nube, controlador de ingreso, CDN o malla de servicios. Si usas a ciegas $remote_addr, podrías estar limitando la dirección del proxy. Eso significa:
- Un cliente ruidoso puede estrangular a todos.
- O estableces límites tan altos que son ineficaces, y los atacantes atraviesan sin problema.
Arréglalo con el módulo Real IP, pero sé estricto sobre la confianza. Acepta X-Forwarded-For (o PROXY protocol) solo de rangos IP de proxy conocidos. Si confías en todo internet, cualquier cliente puede forjar un X-Forwarded-For y obtener identidades frescas infinitas.
Registro y observabilidad: hacer los 429 accionables
El formato de log de acceso por defecto no te dirá por qué una petición fue limitada. Necesitas:
- la clave usada para limitar (o al menos la IP del cliente tras el procesamiento real-IP)
- tiempo de la petición y tiempo upstream
- código de estado y bytes enviados
- ID de petición para correlación
- qué limitador se disparó
Nginx expone $limit_req_status que es extremadamente útil. Regístralo. Si es “REJECTED”, sabes que el limitador lo hizo. Si es “PASSED” pero los clientes aún se quejan, tu cuello de botella está en otra parte.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estas son las cosas que realmente ejecuto en Ubuntu cuando alguien dice “la limitación de tasa está bloqueando usuarios reales” o “el sitio está siendo golpeado”. Cada tarea incluye qué significa la salida y qué decisión tomar a partir de ella.
Tarea 1: Confirmar versión de Nginx y módulos compilados
cr0x@server:~$ nginx -V
nginx version: nginx/1.24.0 (Ubuntu)
built with OpenSSL 3.0.13 30 Jan 2024
configure arguments: ... --with-http_realip_module --with-http_limit_req_module --with-http_limit_conn_module ...
Qué significa: Tienes los módulos Real IP, limit_req y limit_conn compilados (común en paquetes de Ubuntu).
Decisión: Si falta --with-http_realip_module y estás detrás de un proxy, detente y corrige la estrategia de empaquetado (usa el paquete de la distro o recompila). Sin real IP, la selección de clave puede ser inútil.
Tarea 2: Validar la configuración activa (no la que crees que está activa)
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Qué significa: La sintaxis es válida.
Decisión: Si esto falla, no recargues. Arregla la configuración primero. Cambios de limitación que no cargan limpiamente se convierten en folklore de “cambiamos algo” sin efecto real.
Tarea 3: Volcar la configuración cargada completa para ver includes y overrides
cr0x@server:~$ sudo nginx -T | sed -n '1,120p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration file /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
...
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
Qué significa: Ahora puedes localizar dónde se definen limit_req y limit_req_zone.
Decisión: Si encuentras múltiples limitadores conflictivos entre conf.d y sites-enabled, consolida. Límites fragmentados son cómo se obtiene “funciona en staging” y “bloquea usuarios en prod”.
Tarea 4: Comprobar si estás limitando el balanceador (sanidad de IP real)
cr0x@server:~$ sudo tail -n 5 /var/log/nginx/access.log
10.10.5.12 - - [30/Dec/2025:10:10:12 +0000] "GET /login HTTP/2.0" 200 512 "-" "Mozilla/5.0 ..."
10.10.5.12 - - [30/Dec/2025:10:10:12 +0000] "GET /api/me HTTP/2.0" 429 169 "-" "Mozilla/5.0 ..."
10.10.5.12 - - [30/Dec/2025:10:10:13 +0000] "GET /api/me HTTP/2.0" 429 169 "-" "Mozilla/5.0 ..."
Qué significa: Si todos los clientes aparecen con la misma dirección RFC1918 (como 10.10.5.12), ese probablemente sea tu proxy/LB.
Decisión: Arregla Real IP antes de tocar umbrales. De otra forma estarás ajustando para el “cliente” equivocado y bloquearás a todos a la vez.
Tarea 5: Verificar que Real IP está configurado y la confianza es estrecha
cr0x@server:~$ sudo nginx -T | grep -E 'real_ip_header|set_real_ip_from|real_ip_recursive'
real_ip_header X-Forwarded-For;
set_real_ip_from 10.10.0.0/16;
set_real_ip_from 192.0.2.10;
real_ip_recursive on;
Qué significa: Nginx reemplazará $remote_addr usando XFF, pero solo cuando la petición venga de rangos de proxies confiables.
Decisión: Si ves set_real_ip_from 0.0.0.0/0;, efectivamente has invitado a la suplantación. Arregla eso inmediatamente. Tu limitación puede ser eludida por un header.
Tarea 6: Inspeccionar el error log en busca de señales de limit_req
cr0x@server:~$ sudo grep -E 'limiting requests|limit_req' /var/log/nginx/error.log | tail -n 5
2025/12/30 10:10:13 [error] 12410#12410: *991 limiting requests, excess: 5.610 by zone "api_per_ip", client: 203.0.113.55, server: example, request: "GET /api/me HTTP/2.0", host: "example"
Qué significa: Nginx está rechazando por la zona api_per_ip, y muestra el “exceso” calculado. Esto es oro para ajustar.
Decisión: Si los humanos están bloqueados, decide si la clave es incorrecta (NAT/proxy), la tasa es demasiado baja, o la ráfaga es demasiado pequeña.
Tarea 7: Añadir un formato de log que registre el estado del limitador (y comprobar que funciona)
cr0x@server:~$ sudo grep -R "limit_req_status" -n /etc/nginx | head
/etc/nginx/nginx.conf:35:log_format main_ext '$remote_addr $request_id $status $request $limit_req_status';
Qué significa: Tus logs ahora registran si el limitador pasó, retrasó o rechazó una petición.
Decisión: Si no puedes ver $limit_req_status por petición, maldiagnosticarás “429 por upstream” vs “429 por limitador”. Agrégalo antes de ajustar.
Tarea 8: Confirmar quién devuelve 429 (Nginx vs upstream)
cr0x@server:~$ curl -s -D - -o /dev/null https://example/api/me | sed -n '1,12p'
HTTP/2 429
server: nginx
date: Tue, 30 Dec 2025 10:12:10 GMT
content-type: text/html
content-length: 169
Qué significa: La respuesta viene de Nginx (ver server: nginx). Si tu upstream también devuelve 429, necesitarás headers/campos de log adicionales para distinguir.
Decisión: Si es Nginx, ajusta Nginx. Si es el upstream, no pierdas tiempo ajustando limit_req.
Tarea 9: Medir rápidamente tasas de petición actuales por IP cliente
cr0x@server:~$ sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
8421 203.0.113.55
2210 198.51.100.27
911 203.0.113.101
455 192.0.2.44
Qué significa: Qué IPs son las más calientes en tus logs para la ventana muestreada (no es una tasa real, pero es un mapa rápido de calor).
Decisión: Si unas pocas IPs dominan, los límites por IP son probablemente efectivos. Si ves muchas IPs con recuentos bajos, estás ante tráfico distribuido y los límites por IP no ayudarán mucho.
Tarea 10: Comprobar conexiones activas y quién las mantiene
cr0x@server:~$ sudo ss -Htn state established '( sport = :443 )' | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -nr | head
120 203.0.113.55
48 198.51.100.27
11 192.0.2.44
Qué significa: Clientes con muchas conexiones TCP establecidas a 443.
Decisión: Si un cliente mantiene cientos/miles de conexiones, limit_conn es mejor herramienta que limit_req. Si las conexiones son pocas pero las peticiones son muchas, céntrate en limit_req y caching.
Tarea 11: Comprobar si HTTP/2 está habilitado (afecta el comportamiento de ráfagas)
cr0x@server:~$ sudo nginx -T | grep -R "listen 443" -n /etc/nginx/sites-enabled | head
/etc/nginx/sites-enabled/example.conf:12: listen 443 ssl http2;
Qué significa: La multiplexación HTTP/2 está en juego.
Decisión: Espera patrones “ráfaga” de navegadores normales. Incrementa el burst para endpoints front-end, o aplica limitación más específica a caminos costosos.
Tarea 12: Rastrear 429 a lo largo del tiempo desde logs (vista de tendencia barata)
cr0x@server:~$ sudo awk '$9==429 {c++} END{print c+0}' /var/log/nginx/access.log
317
Qué significa: Conteo de respuestas 429 en el archivo de log actual.
Decisión: Si este número es no trivial, necesitas clasificar: ¿son esperados (bots) o inesperados (clientes)? Añade desagregación por endpoint a continuación.
Tarea 13: Desglosar 429 por ruta para encontrar qué estás bloqueando
cr0x@server:~$ sudo awk '$9==429 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
210 /api/me
61 /login
24 /search
12 /password/reset
Qué significa: Qué endpoints están siendo limitados.
Decisión: Si /api/me recibe límites y eso lo llama tu SPA en cada carga, tu baseline es demasiado estricto o tu clave es incorrecta (NAT/proxy). Si /login está muy limitado, eso puede ser correcto (credential stuffing) pero verifica las quejas de clientes.
Tarea 14: Confirmar configuración de zonas y tasas (encontrar la política que aplicas)
cr0x@server:~$ sudo nginx -T | grep -E 'limit_req_zone|limit_req[^_]' -n | head -n 30
45: limit_req_zone $binary_remote_addr zone=login_per_ip:20m rate=5r/m;
46: limit_req_zone $binary_remote_addr zone=api_per_ip:50m rate=10r/s;
112: limit_req zone=login_per_ip burst=3;
166: limit_req zone=api_per_ip burst=20 nodelay;
Qué significa: Tu login está limitado a 5 peticiones por minuto por IP. Tu API es 10 peticiones por segundo por IP. Esas son políticas muy distintas.
Decisión: Decide si estos números reflejan la realidad. 10r/s por IP puede ser demasiado bajo si existen clientes corporativos tras NAT. 5r/m en login puede ser demasiado bajo si la página de login dispara varias llamadas relacionadas con auth.
Tarea 15: Recargar de forma segura y confirmar que los workers lo recogen
cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ systemctl status nginx --no-pager | sed -n '1,12p'
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-12-30 09:55:01 UTC; 18min ago
Docs: man:nginx(8)
Qué significa: La recarga fue exitosa y Nginx se mantuvo en ejecución.
Decisión: Si ves recargas fallidas, deja de iterar. Arregla la higiene de configuración primero. Afinar límites en un pipeline de despliegue roto es solo arte performance.
Guion de diagnóstico rápido
Cuando los 429s suben de golpe o los usuarios se quejan, no divagues. Haz esto en orden. El objetivo es encontrar el cuello de botella (política, clave o capacidad) en menos de 10 minutos.
Primero: confirma quién genera los 429 y por qué
- Revisa logs de acceso por estado 429 y rutas principales (Tarea 12, Tarea 13).
- Revisa el error log por mensajes de
limiting requests(Tarea 6).- Si el error log muestra rechazos del limitador: es política de Nginx.
- Si no: el 429 puede venir del upstream o de otra capa del gateway.
- Chequea headers con curl (Tarea 8) para ver si Nginx sirve el 429.
Segundo: verifica que la clave sea sensata en tu topología
- Mira las IPs cliente registradas (Tarea 4).
- Si ves IPs del LB: la configuración de IP real falta o está rota.
- Si ves un pequeño conjunto de IPs NAT: limitar por IP puede castigar a muchos usuarios.
- Verifica los límites de confianza de Real IP (Tarea 5). Asegura que solo confíes en tus proxies.
Tercero: decide si los umbrales o la ráfaga son incorrectos (o si necesitas límites específicos por endpoint)
- Inspecciona zonas actuales y tasas (Tarea 14).
- Revisa uso de HTTP/2 (Tarea 11). Si está habilitado, incrementa burst para endpoints orientados al navegador.
- Comprueba si el problema son conexiones o peticiones (Tarea 10). Usa
limit_connpara acaparadores de conexión.
Cuarto: verifica que no sea un problema de capacidad que se disfraza de limitación
Cuando los upstreams se ralentizan, los reintentos aumentan, lo que incrementa la tasa de peticiones y dispara tu limitador. El limitador no está “equivocado”; está exponiendo un problema real de capacidad.
Usa tus métricas existentes, pero en la caja puedes al menos registrar campos de timing del upstream y detectar upstreams lentos en los logs de acceso. Si los tiempos upstream son altos, ajusta el upstream primero o añade caching, y luego revisa el limitador.
Tres mini-historias corporativas desde el frente
Mini-historia 1: El incidente causado por una suposición errónea (NAT no es “raro”)
Un SaaS B2B de tamaño medio desplegó Nginx limit_req con una regla ordenada: 5 peticiones por segundo por IP en /api/. En staging parecía perfecto. En producción, todo iba bien hasta el lunes por la mañana en Norteamérica.
Los tickets de Soporte llegaron en oleadas: “La app se queda cargando”, “El dashboard no carga”, “429s aleatorios”. Ingeniería miró los dashboards y vio un aumento leve de tráfico—nada dramático. El comandante del incidente sospechó inicialmente de un mal deploy. El rollback no ayudó.
En el borde, los access logs contaron la historia: múltiples clientes de pago aparecían desde el mismo puñado de IPs. Esas no eran “personas”. Eran gateways de egress corporativos y pools NAT de operadores. Una sola oficina con cientos de empleados compartiendo una IP de salida ahora podía “gastar” colectivamente solo 5 peticiones por segundo en toda la app.
El arreglo inmediato fue feo pero efectivo: aumentar sustancialmente el límite por IP y añadir reglas más estrictas solo para endpoints sensibles (login, reset de contraseña). La corrección real fue una mejor clave: por token de API para llamadas autenticadas y por cookie de sesión para tráfico de navegador cuando fue posible.
Tras el incidente añadieron un ítem a la checklist previa: “Mostrar distribución de IPs únicas vs IDs autenticados durante el pico”. Pasó a ser parte rutinaria de la planificación de capacidad, no un pensamiento tardío.
Mini-historia 2: La optimización que salió mal (nodelay en todas partes)
Otra compañía tenía un problema con clientes ruidosos. Alguien sugirió: “Usa burst con nodelay para que usuarios reales no sientan throttling.” Lo aplicaron ampliamente: todos los endpoints API, todos los clientes, ráfagas generosas, nodelay activado.
La experiencia de usuario mejoró en el camino feliz. Luego un partner de integración configuró mal una política de reintento. Su cliente empezó a golpear timeouts y reintentar agresivamente en paralelo. Con nodelay y una gran ráfaga, Nginx encaminó felices microráfagas grandes directamente a los servicios upstream, que ya estaban luchando.
Los upstreams cayeron, lo que aumentó timeouts, lo que aumentó reintentos, lo que aumentó ráfagas. Un bucle de retroalimentación con una fachada de buen UX. El monitoring mostraba que Nginx “estaba bien” y la CPU estable, pero las tasas de error y la latencia upstream se dispararon.
La solución final fue una política más matizada: mantener nodelay para algunos endpoints de solo lectura que necesitaban respuesta rápida, pero quitar nodelay para endpoints costosos y reducir tamaños de burst. También introdujeron protección en colas upstream y mejores valores por defecto de timeout/retry en el SDK cliente.
La moraleja no fue “nodelay es malo”. Fue que suavizar y la experiencia de usuario deben alinearse con la capacidad del backend, no con el pensamiento deseoso.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día (registrar estado del limitador)
Un equipo fintech tenía la costumbre que parecía aburrida en code review: cada vez que añadían un limitador, también añadían campos de log para $request_id, $limit_req_status y la clave usada (hasheada si era necesario). Sin excepciones.
Una noche vieron un pico de 429s y una caída en conversiones. La sospecha inicial fue un ataque de bots. No lo era. Los logs mostraron que la mayoría de 429s eran “PASSED” y el upstream devolvía 429—lo que significaba que la propia aplicación estaba limitando porque un proveedor de pagos downstream devolvía errores y su disyuntor se disparó.
Gracias a los logs del edge explícitos, no perdieron una hora relajando límites de Nginx e invitando abuso real. Arreglaron la lógica de failover del proveedor de pagos, ajustaron ventanas de reintento y dejaron la política del edge intacta.
Ese incidente no se convirtió en un festival de culpas entre equipos. Fue una reparación de 40 minutos con una línea temporal clara. El logging aburrido venció a la suposición dramática.
Errores comunes: síntomas → causa raíz → solución
1) “Todos los usuarios reciben 429 al mismo tiempo”
Síntomas: 429s generalizados de repente; usuarios en varias regiones afectados; logs muestran la misma IP cliente.
Causa raíz: estás limitando la IP del load balancer/proxy porque Real IP no está configurado (o está mal configurado).
Solución: configura real_ip_header y estrecha set_real_ip_from a rangos de proxy confiables. Luego usa $binary_remote_addr (tras el procesamiento real IP) como clave.
2) “Clientes corporativos se quejan, usuarios domésticos están bien”
Síntomas: redes de oficina ven errores; usuarios móviles y residenciales no; la lista top de IPs muestra unas pocas IPs con tráfico pesado.
Causa raíz: límites por IP castigan redes NAT y puntos de egress de VPN.
Solución: para endpoints autenticados, limita por API key o sesión. Para anónimos, sube los límites por IP y restringe límites estrictos a endpoints costosos específicos.
3) “El login está roto, pero solo a veces”
Síntomas: fallos intermitentes de login; usuarios tienen éxito después de esperar; picos durante campañas de marketing.
Causa raíz: el flujo de login hace varias llamadas (fetch CSRF, preflight MFA, telemetría) y dispara un limitador estricto con ráfaga demasiado pequeña.
Solución: aplica límites al endpoint POST de credenciales, no a cada petición bajo /login. Aumenta ligeramente el burst; considera retrasar (sin nodelay) en lugar de rechazar por un exceso menor.
4) “Habilitamos limitación, pero los ataques siguen afectando”
Síntomas: carga del backend sigue alta; logs del limitador muestran bajos rechazos; muchas IPs de origen.
Causa raíz: tráfico distribuido (botnets, proxies residenciales) hace ineficaces los límites por IP; o la zona es demasiado pequeña y hace thrashing.
Solución: añade límites basados en identidad (API key, token), políticas específicas por endpoint, caching y protecciones upstream. Aumenta tamaños de zona y monitoriza efectos de expulsión.
5) “Después de ajustar, el upstream comenzó a fallar más”
Síntomas: menos 429s, pero más 5xx desde upstream; la latencia aumenta.
Causa raíz: aflojaste límites sin capacidad; o activaste nodelay con gran burst y reenviabas picos al upstream.
Solución: reintroduce suavizado (quita nodelay) para endpoints costosos; establece bursts razonables; ajusta timeouts upstream; añade caching y colas.
6) “La limitación se elude”
Síntomas: el limitador parece inefectivo; los atacantes aparecen como muchas IPs; logs muestran valores XFF extraños.
Causa raíz: confiar en X-Forwarded-For desde fuentes no confiables (set_real_ip_from 0.0.0.0/0 o equivalente).
Solución: confía solo en proxies conocidos; considera PROXY protocol si está soportado; valida que la IP del cliente cambie solo cuando las peticiones provienen de redes proxy confiables.
Listas de verificación / plan paso a paso
Esta es la secuencia que suele funcionar en entornos reales sin convertir tu edge en una máquina tragaperras.
Plan paso a paso: implementar limitación de tasa sin bloquear humanos
- Arregla la identidad del cliente en el edge.
- ¿Detrás de un proxy? Configura Real IP con confianza estrecha.
- Decide qué significa “cliente”: IP para anónimos, token para autenticados.
- Clasifica endpoints.
- Endpoints de auth (login, reset) = estrictos
- Endpoints costosos (búsqueda, reports) = algo estrictos
- Endpoints de lectura baratos = moderados
- Assets estáticos = típicamente sin limitación
- Crea zonas separadas por clase.
- No reutilices una zona para todo. Ocultarás qué comportamiento es abusivo.
- Define tasas iniciales con sesgo a menos falsos positivos.
- Empieza más alto de lo que piensas y reduce según abuso observado.
- Usa burst para proteger humanos de clientes con picos.
- Registra estado del limitador e IDs de petición.
- Si no lo registras, no podrás ajustarlo.
- Despliega gradualmente.
- Aplica a una clase de endpoints primero (login es buen candidato).
- Observa tasa de 429, conversión y logs de errores.
- Decide el comportamiento de respuesta.
- Usa 429 para throttling por equidad.
- Considera retrasar (sin
nodelay) para fuerza bruta para gastar tiempo del atacante.
- Prueba con forma de tráfico realista.
- Navegadores (HTTP/2) y apps móviles son ráfaga.
- Incluye reintentos en pruebas de carga, porque en producción estarán presentes.
Una configuración base sólida (con opinión)
Este ejemplo asume:
- Estás detrás de un LB confiable en
10.10.0.0/16 - Quieres protección estricta de login por IP
- Quieres protección de API por API key cuando esté presente, si no por IP
- Quieres logs que te digan qué pasó
cr0x@server:~$ sudo sed -n '1,220p' /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main_ext '$remote_addr $request_id $time_local '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'lrs=$limit_req_status '
'xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main_ext;
error_log /var/log/nginx/error.log warn;
real_ip_header X-Forwarded-For;
set_real_ip_from 10.10.0.0/16;
real_ip_recursive on;
# Key selection for APIs: API key if present, else IP.
map $http_x_api_key $api_limit_key {
default $http_x_api_key;
"" $binary_remote_addr;
}
# Whitelist internal monitoring and office VPN (example).
map $binary_remote_addr $is_whitelisted {
default 0;
192.0.2.44 1;
198.51.100.77 1;
}
# Zones: size according to expected unique keys.
limit_req_zone $binary_remote_addr zone=login_per_ip:20m rate=5r/m;
limit_req_zone $api_limit_key zone=api_per_key:100m rate=20r/s;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
cr0x@server:~$ sudo sed -n '1,220p' /etc/nginx/sites-enabled/example.conf
server {
listen 443 ssl http2;
server_name example;
# Default: do not rate limit everything. Target specific locations.
location = /login {
if ($is_whitelisted) { break; }
limit_req zone=login_per_ip burst=6;
proxy_pass http://app_upstream;
}
location = /password/reset {
if ($is_whitelisted) { break; }
limit_req zone=login_per_ip burst=3;
proxy_pass http://app_upstream;
}
location ^~ /api/ {
if ($is_whitelisted) { break; }
limit_req zone=api_per_key burst=40 nodelay;
proxy_pass http://app_upstream;
}
location / {
proxy_pass http://app_upstream;
}
}
Por qué esta base funciona:
- Login se limita por IP a escala humana (por minuto), permitiendo ráfagas menores.
- API se limita por clave de cliente cuando está disponible, lo cual evita el dolor de NAT.
- La limitación está dirigida; no rompes la carga de assets ni la navegación básica.
- Los logs capturan el estado del limitador para que puedas ajustar con evidencia.
Chiste 2/2: Si limitas tus propias comprobaciones de salud, felicitaciones—acabas de inventar “auto-cuidado” para servidores, y lo tomarán literalmente.
Preguntas frecuentes
1) ¿Cuál es la diferencia entre limit_req y limit_conn?
limit_req limita la tasa de peticiones (r/s o r/m). limit_conn limita conexiones concurrentes. Usa límites de petición para inundaciones y equidad; usa límites de conexión para acaparamiento de conexiones y streams de larga duración.
2) ¿Debería limitar globalmente en location /?
Casi nunca. Castigarás el comportamiento normal del navegador y los reintentos. Limita endpoints costosos o sensibles, luego añade una base ligera solo si realmente la necesitas.
3) ¿Por qué usuarios reales se bloquean cuando los bots no?
Porque tu clave es incorrecta (NAT/proxy), tus umbrales son demasiado bajos para patrones de ráfaga normales, o los bots están distribuidos en muchas IPs mientras tus usuarios están concentrados detrás de egress compartidos.
4) ¿Es $binary_remote_addr mejor que $remote_addr?
Para claves de limitador, sí. Es una representación binaria compacta y se recomienda por eficiencia de memoria compartida. Pero solo ayuda si Real IP está correctamente configurado detrás de proxies.
5) ¿Cuándo debo usar nodelay?
Úsalo cuando quieras permitir ráfagas breves sin añadir latencia y tu upstream pueda soportar picos cortos. Evítalo en endpoints costosos o cuando el upstream ya es inestable.
6) ¿Cómo evito castigar a usuarios detrás de NAT?
Limita el tráfico autenticado por identidad de cliente (API key, token, sesión) y mantén los límites por IP principalmente para endpoints anónimos sensibles. También evita umbrales por IP minúsculos en APIs generales.
7) ¿Puedo “whitelist” algunos clientes de forma segura?
Sí, pero mantenlo estrecho y auditable (IPs de monitoring, egress de VPN de oficina, IPs de partners concretos). Las listas blancas tienden a crecer y convertirse en un riesgo de seguridad si no se gestionan.
8) ¿Cómo sé si la zona del limitador es demasiado pequeña?
Verás limitación inconsistente bajo alta cardinalidad de claves únicas y patrones que no tienen sentido. Prácticamente: si tienes un endpoint público y una zona pequeña, aumenta y observa si el comportamiento del limitador se estabiliza.
9) ¿Devolver 429 es siempre la elección correcta?
Para throttling por equidad, sí. Para disuadir fuerza bruta, retrasar (sin nodelay) puede ser más efectivo. Para protección en meltdown, 503 con semánticas de backoff también puede tener sentido, pero sé intencional.
10) ¿Nginx puede hacer limitación por nombre de usuario en intentos de login?
No de manera limpia con variables stock, porque el nombre de usuario suele estar en el cuerpo POST. Haz per-IP en Nginx y controles por nombre de usuario y cuenta en la capa de aplicación.
Conclusión: pasos prácticos siguientes
Si quieres limitación de tasa que no deje fuera a usuarios reales, haz tres cosas antes de tocar un solo número:
- Arregla la identidad en el edge. IP real detrás de proxies y claves de identidad para APIs autenticadas.
- Limita por comportamiento, no por sitio. Zonas y políticas separadas para login/reset/búsqueda/API. Deja lo demás en paz salvo que tengas evidencia.
- Haz los 429 depurables. Registra
$limit_req_status, añade IDs de petición y revisa el error log por mensajes del limitador.
Luego afina como un adulto: mide, cambia una cosa, recarga con seguridad y observa resultados. Tu objetivo no es “menos peticiones”. Tu objetivo es “menos peticiones malas, mismos usuarios buenos”. Ese es todo el trabajo.