NGINX y el “cubo que gotea” que protege tu web: cómo limitar peticiones sin romper la experiencia

NGINX se ha convertido en la puerta de acceso de miles de servicios web. Desde esa posición, una de sus funciones más útiles —y con frecuencia mal entendida— es limitar la tasa de peticiones para evitar desbordes, frenar ataques de fuerza bruta y suavizar picos de tráfico que, de otro modo, saturarían las aplicaciones de detrás. Bien configurada, esta capacidad reduce errores 5xx, mantiene a raya los tiempos de respuesta y ayuda a distinguir entre el comportamiento normal de los navegadores (siempre “ráfagas”) y patrones que conviene frenar.

Nota previa importante. La propia comunidad de NGINX ha aclarado que la forma de calcular burst y delay puede inducir a confusión si se interpreta como promedios rígidos por segundo. NGINX contabiliza a resolución de milisegundo con ventana deslizante, no como media estática a lo largo de un periodo. Para el detalle fino de cada parámetro, conviene revisar la documentación oficial de nginx.org.


La metáfora del “cubo que gotea” aplicada a HTTP

El mecanismo que usa NGINX se basa en el algoritmo de “leaky bucket”: el agua es el stream de peticiones entrantes, el cubo es la cola FIFO y el “goteo” regula a qué ritmo se entregan peticiones al backend. Si entran más de las que pueden salir, el cubo rebosa y hay solicitudes que se rechazan. La gracia está en mantener un flujo estable hacia la aplicación aun cuando el navegador lance varias descargas simultáneas (HTML, CSS, JS, imágenes) o cuando un formulario reciba varios intentos en pocos milisegundos.

En términos prácticos:

  • Protección de formularios sensibles: login, reset de contraseña o endpoints de autenticación dejan de ser un coladero para ataques de fuerza bruta.
  • Auto-defensa ante picos: una oleada de clics durante una promoción no tiene por qué convertir el backend en una cola infinita.
  • Autopsia más clara: con logging, es más sencillo identificar qué URLs sufren abuso o qué clientes generan ráfagas anómalas.

Dos directivas que lo cambian todo: limit_req_zone y limit_req

La limitación se configura con dos piezas:

  1. limit_req_zone define la zona compartida de memoria, la clave por la que se agrupan las peticiones y la tasa autorizada. Suele declararse en el bloque http para reutilizarla en varios server o location.
  2. limit_req aplica la política en el contexto deseado (un location, un server, etc.).

Ejemplo mínimo para proteger /login/ a 10 peticiones por segundo por IP:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=mylimit;
        proxy_pass http://my_upstream;
    }
}
Lenguaje del código: PHP (php)

Claves del ejemplo:

  • Key: $binary_remote_addr agrupa por IP de cliente en binario (ahorra memoria frente a $remote_addr).
  • Zone: mylimit:10m reserva 10 MiB en memoria compartida; como referencia, 1 MiB almacena ~16.000 direcciones, así que 10 MiB dan para ~160.000 IPs. Si se agota la zona, NGINX purga entradas antiguas; si aun así no cabe la nueva, devuelve 503.
  • Rate: 10r/s equivale, de forma aproximada, a 1 petición cada 100 ms. Al medir en milisegundos y con ventana deslizante, se rechaza lo que rompe ese ritmo, salvo que se configure burst.

Ráfagas controladas: burst para no castigar a los navegadores

Sin burst, dos peticiones separadas por menos de 100 ms (en el ejemplo) harán que la segunda reciba 503. Como la navegación real siempre es “ráfaga”, conviene permitir un colchón:

location /login/ {
    limit_req zone=mylimit burst=20;
    proxy_pass http://my_upstream;
}
Lenguaje del código: JavaScript (javascript)
  • burst=20: habilita 20 “huecos” en cola por encima de la tasa. Las peticiones “demasiado pronto” se encolan y NGINX las libera siguiendo el ritmo marcado (aprox. 1 cada 100 ms en el ejemplo).
  • Si llegan 21 a la vez, la 1.ª pasa y 20 entran en cola; la 22.ª se rechaza con 503.

Problema: espaciar 20 peticiones a 100 ms significa que la última esperará ~2 s; puede ser inútil para el usuario.


Ráfagas sin añadir latencia: nodelay

Para evitar que la cola introduzca esperas visibles, existe nodelay:

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    proxy_pass http://my_upstream;
}
Lenguaje del código: JavaScript (javascript)
  • Con nodelay, NGINX no “gotea” las peticiones encoladas: las reenvía de inmediato mientras haya “slots” disponibles en el burst y marca esos slots como ocupados durante el intervalo correspondiente (100 ms en el ejemplo).
  • Si entran 21 de golpe, se envían las 21 al instante y se marcan 20 slots. Cada 100 ms se libera 1.
  • Si llegan otras 20 tras 101 ms, solo hay 1 slot libre: 1 se envía, 19 se rechazan.

Efecto neto: se mantiene la tasa efectiva (10 r/s), pero sin añadir retardo a los picos “permitidos”.

Recomendación habitual: usar burst + nodelay salvo que exista una razón concreta para espaciar la cola.


Dos etapas de control: delay para combinar “pase libre” y “freno suave”

Desde NGINX 1.15.7 se puede definir una primera porción del burst sin demora y, a partir de un umbral, aplicar retraso para no superar la tasa; si se rebasa el burst total, se rechaza. Ejemplo típico:

limit_req_zone $binary_remote_addr zone=ip:10m rate=5r/s;

server {
    location / {
        limit_req zone=ip burst=12 delay=8;
        proxy_pass http://website;
    }
}
Lenguaje del código: PHP (php)
  • Tasa base: 5 r/s.
  • Ráfaga total: 12.
  • delay=8: las primeras 8 por encima de la tasa no se demoran; las siguientes 4 se espacian para cumplir los 5 r/s; a partir de ahí, se rechazan hasta volver a tener “hueco”.

Recuerdo de la nota inicial: la mecánica real opera a milisegundos con ventana deslizante; siempre conviene confirmar el comportamiento exacto en la documentación.


Listas de confianza y segmentación con geo y map

Es habitual permitir más ritmo a redes internas o socios, y aplicar límites más estrictos al resto. Puede hacerse combinando geo y map para construir una clave condicional:

geo $limit {
    default 1;
    10.0.0.0/8      0;
    192.168.0.0/24  0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;

server {
    location / {
        limit_req zone=req_zone burst=10 nodelay;
        # ...
    }
}
Lenguaje del código: PHP (php)
  • Las subredes internas reciben $limit=0 y, mediante map, clave vacía ("").
  • Cuando la clave de limit_req_zone es cadena vacía, no se aplica el límite.
  • El resto de IPs usa $binary_remote_addr y queda limitado a 5 r/s con 10 de burst.

Varias políticas en el mismo location: aplica la más estricta

Se pueden encadenar varios limit_req sobre la misma ruta. Si una política retrasa y otra también, prevalece la mayor demora; si una rechaza, el rechazo se impone aunque otra permita pasar.

Extensión del caso anterior para dar un “trato VIP” a las direcciones de confianza:

http {
    limit_req_zone $limit_key         zone=req_zone:10m    rate=5r/s;
    limit_req_zone $binary_remote_addr zone=req_zone_wl:10m rate=15r/s;

    server {
        location / {
            limit_req zone=req_zone     burst=10 nodelay;   # resto
            limit_req zone=req_zone_wl  burst=20 nodelay;   # allowlist
        }
    }
}
Lenguaje del código: PHP (php)
  • Las redes de confianza no coinciden con req_zone (clave vacía) pero con req_zone_wl: quedan en 15 r/s.
  • El resto coincide con ambas, por lo que manda la más restrictiva: 5 r/s.

Registros, códigos de respuesta y otras piezas relacionadas

Cómo queda el registro cuando se frena o se rechaza

Por defecto, NGINX escribe una línea error cuando rechaza y una de nivel inferior cuando demora. Ejemplo:

YYYY/MM/DD HH:MM:SS [error] 120315#0: *32086 limiting requests, excess: 1.000 by zone "mylimit", client: 192.168.1.2, server: ejemplo.com, request: "GET / HTTP/1.1", host: "ejemplo.com"
Lenguaje del código: PHP (php)
  • excess indica cuánto se excede la tasa por milisegundo.
  • Para ajustar el nivel de registro, usar limit_req_log_level:
location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_log_level warn;  # en vez de error
    proxy_pass http://my_upstream;
}
Lenguaje del código: PHP (php)

Elegir el código devuelto cuando se excede el límite

Por defecto se devuelve 503 (Service Temporarily Unavailable). Se puede cambiar con limit_req_status:

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_status 444;   # cierra sin respuesta formal
}
Lenguaje del código: PHP (php)

Bloquear por completo una ruta concreta

Si el objetivo es denegar todo en un endpoint determinado:

location /foo.php {
    deny all;
}

Tamaño de la zona y memoria: evitar sustos

Recordatorio útil: 1 MiB ≈ 16.000 direcciones IP en la zona de limit_req. Si la web recibe muchos visitantes únicos o ataques distribuidos, una zona pequeña puede quedarse sin hueco. Cuando NGINX necesita registrar una IP nueva:

  • Purga entradas antiguas no usadas en 60 s (hasta dos por cada alta).
  • Si aún no cabe, responde 503.

Pista de diagnóstico: si aparecen 503 sólo bajo mucho tráfico y el hit ratio de caché es alto, revise el tamaño de la zone antes de culpar al backend.


Dónde aplicarlo para que sume y no reste

  • Rutas sensibles: /login/, /reset-password/, /wp-login.php, /xmlrpc.php, APIs de autenticación y endpoints con efectos de pago.
  • Rutas “calientes”: búsquedas, promociones, operaciones que “tiran” de base de datos.
  • Agregadores y scrapers: en listados y feeds, el límite desacelera bots ansiosos sin afectar a usuarios reales.

Evite, en cambio, desplegar tasas estrictas sobre recursos estáticos o CDN de terceros —no tiene sentido “acelerar y frenar” bytes que puede servir cacheados—.


Errores habituales (y cómo evitarlos)

  1. Olvidar burst y castigar a usuarios reales: navegadores y apps móviles disparan ráfagas. Añada burst y, salvo necesidad contraria, nodelay.
  2. Tasas irreales: 5 r/s puede ser lógico para login; no tanto para una portada con 12 recursos. Ajuste rate, burst y, si procede, delay.
  3. Zona minúscula: una zona de 1–2 MiB puede saturarse en sitios concurridos. Dimensione en función de la cardinalidad de IPs.
  4. Límites globales que tapan todo: aplique por ruta o por servidor; las políticas “a martillo” terminan penalizando a quien no debe.
  5. No registrar: sin logs, el ajuste es a ciegas. Active niveles coherentes con limit_req_log_level.
  6. Confundir 429 con 503: el límite de NGINX devuelve 503 por defecto; si su capa de negocio espera 429 (Too Many Requests), conviene re-mapear el código o gestionarlo aguas arriba.

Propuesta de despliegue en tres pasos

  1. Medir
    • Cuente recursos por página tipo (¿4–6? ¿hasta 12?).
    • Revise logs de formularios y de APIs con más abuso.
    • Determine cardinalidad de IPs (pico de únicas).
  2. Configurar
    • Declare limit_req_zone en http con tasa realista y zona suficiente.
    • Aplique limit_req en rutas sensibles con burst + nodelay.
    • Si su patrón de página es “varias descargas de golpe”, considere delay para un “pase libre” corto sin demoras y un “freno suave” después.
  3. Observar y ajustar
    • Vigile 503 asociados a la zona.
    • Ajuste tasa, burst o zona según el patrón real.
    • Añada excepciones con geo/map para redes de confianza.

Ejemplo completo con rutas críticas y diferenciación por redes

http {
    # 1) Clave condicional: allowlist sin límite, resto por IP
    geo $is_trusted {
        default        1;
        10.0.0.0/8     0;
        192.168.0.0/24 0;
    }

    map $is_trusted $rate_key {
        0 "";
        1 $binary_remote_addr;
    }

    # 2) Zonas: una general y otra más permisiva para trusted si se desea
    limit_req_zone $rate_key           zone=zone_login:10m rate=5r/s;
    limit_req_zone $binary_remote_addr zone=zone_public:20m rate=10r/s;

    server {
        listen 80;

        # /login: frenar fuerza bruta con ráfaga corta sin demora
        location /login/ {
            limit_req zone=zone_login burst=8 nodelay;
            limit_req_log_level warn;
            proxy_pass http://auth_upstream;
        }

        # Página principal y listados: ráfaga mayor, sin demorar
        location / {
            limit_req zone=zone_public burst=20 nodelay;
            proxy_pass http://app_upstream;
        }

        # Bloquear por completo scripts que no deben ser públicos
        location ~* \.php$ {
            deny all;
        }
    }
}
Lenguaje del código: PHP (php)

Este esquema protege con 5 r/s el área de login (y 8 de ráfaga “sin freno”), permite 10 r/s en el resto con 20 de burst, y veta directamente la ejecución de PHP en el frontal. Las redes de confianza declaradas en geo no sufren el límite de login al quedar con clave vacía.


Conclusión

La limitación de peticiones en NGINX no es un “truco” de última hora, sino una pieza estructural para preservar la salud de las aplicaciones. Entender tasa, ráfaga y demora —y, sobre todo, aplicarlos en el lugar correcto— permite absorber el comportamiento real de los clientes, frenar abusos y mantener a la aplicación trabajando en su “zona verde”. Si además se segmentan redes de confianza, se dimensiona bien la zona de memoria y se observan los logs, el resultado es una web más estable bajo carga y menos expuesta al ruido de fondo de Internet.


Preguntas frecuentes

¿Cómo elegir una tasa de peticiones por segundo adecuada para un formulario de inicio de sesión en NGINX?
Conviene partir de 4–6 recursos por página y del patrón de su login (¿incluye llamadas extra a APIs?). Un valor conservador para login suele estar en 5 r/s con burst 8–12 y nodelay, ajustando tras observar registros. Si hay dispositivos antiguos o automatizaciones legítimas, amplíe ligeramente el burst.

¿Qué diferencia práctica hay entre burst y nodelay al limitar peticiones en NGINX?
burst define huecos de cola por encima de la tasa. Sin nodelay, NGINX espacia la salida y puede añadir retardo visible. Con nodelay, envía de inmediato mientras haya huecos y marca esos huecos como ocupados durante el intervalo correspondiente, manteniendo la tasa efectiva sin introducir latencia adicional.

¿Cuándo usar delay para aplicar una limitación en dos etapas en NGINX?
Cuando una página típica descarga varias piezas a la vez (por ejemplo, hasta 12 recursos), delay permite que las primeras X (por ejemplo, 8) pasen sin freno y, a partir de ahí, espacia para no superar la tasa (p. ej., 5 r/s). Si se rebasa el burst total, se rechaza hasta recuperar margen.

¿Cómo registrar y diagnosticar rechazos por exceso de peticiones en NGINX sin inundar los logs?
Use limit_req_log_level para bajar el nivel (por ejemplo, a warn) y filtre por zona en su stack de observabilidad. Vigile el campo excess (exceso por milisegundo) y la métrica de 503 por zona. Si crecen sólo en picos, revise tamaño de la zona y burst; si aparecen en horario valle, la tasa es probablemente demasiado baja o hay un cliente mal comportado que requiere reglas específicas.

vía: blog.nginx.org

Suscríbete al boletín SysAdmin

Este es tu recurso para las últimas noticias y consejos sobre administración de sistemas, Linux, Windows, cloud computing, seguridad de la nube, etc. Lo enviamos 2 días a la semana.

¡Apúntate a nuestro newsletter!


– patrocinadores –

Noticias destacadas

– patrocinadores –

¡SUSCRÍBETE AL BOLETÍN
DE LOS SYSADMINS!

Scroll al inicio
×