Limitación de tasa en Nginx y protección contra DDoS
Configura la limitación de tasa en Nginx con limit_req, limit_conn y fail2ban para proteger tu servidor contra ataques de fuerza bruta y DDoS a nivel de aplicación, sin depender de servicios externos.
La limitación de tasa (rate limiting) es tu primera línea de defensa contra ataques de fuerza bruta, abuso de APIs y DDoS a nivel de aplicación. Este tutorial construye tres capas de protección usando solo Nginx y fail2ban. Sin servicios anti-DDoS de terceros, sin tráfico saliendo de tu servidor.
Vas a configurar la limitación de tasa de peticiones (limit_req), el throttling de conexiones (limit_conn) y el bloqueo automático de IPs (fail2ban) en Debian 12 o Ubuntu 24.04.
Requisitos previos:
- Nginx instalado y en ejecución
- Familiaridad básica con la estructura de configuración de Nginx
- Acceso root o sudo
¿Cómo funciona el rate limiting de Nginx?
El rate limiting de Nginx usa el algoritmo de cubo con goteo (leaky bucket) a través de las directivas limit_req_zone y limit_req. Las peticiones entrantes llenan un cubo a la velocidad a la que llegan. El cubo se vacía a una tasa fija que tú defines. Cuando el cubo se desborda, Nginx rechaza las peticiones sobrantes. Esto suaviza los picos de tráfico mientras mantiene una tasa de procesamiento constante.
La implementación abarca dos directivas. limit_req_zone define la zona de memoria compartida que rastrea el estado de cada cliente a través de todos los procesos worker. limit_req aplica el límite a locations específicas.
# En el bloque http: definir la zona
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# En un bloque server o location: aplicar el límite
limit_req zone=api;
La clave $binary_remote_addr almacena cada IP de cliente en formato binario compacto (4 bytes para IPv4, 16 bytes para IPv6). Una zona de 10 MB puede contener unas 160.000 direcciones IPv4 o 80.000 direcciones IPv6. Para la mayoría de servidores, 10m es suficiente.
El parámetro rate acepta peticiones por segundo (r/s) o por minuto (r/m). Nginx lo rastrea internamente en milisegundos. Una tasa de 10r/s significa una petición permitida cada 100 ms.
¿Cómo configuro limit_req_zone y limit_req?
Crea un archivo de configuración dedicado al rate limiting para mantener las cosas modulares:
sudo nano /etc/nginx/conf.d/rate-limiting.conf
# Zonas de memoria compartida - definidas a nivel http
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
# Devolver 429 en lugar del 503 por defecto
limit_req_status 429;
# Registrar eventos de rate limiting en nivel warn (retrasos en nivel notice)
limit_req_log_level warn;
Luego aplica las zonas en tu bloque server:
sudo nano /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
# Límite de tasa general para todas las peticiones
limit_req zone=general burst=20 nodelay;
location /login {
# Límite estricto en el endpoint de login
limit_req zone=login burst=3 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
# Límite más alto para consumidores de API
limit_req zone=api burst=50 delay=30;
proxy_pass http://127.0.0.1:3000;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}
Prueba la configuración y recarga:
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx
Cuando una directiva limit_req se define en un bloque location, anula cualquier limit_req heredado del nivel server. La zona general se aplica a / pero no a /login ni a /api/ porque esas locations tienen sus propias directivas limit_req. Si necesitas que ambas zonas se apliquen, añade varias líneas limit_req en el mismo bloque.
¿Qué hacen burst, nodelay y delay?
El parámetro burst controla cuántas peticiones excedentes Nginx pone en cola en lugar de rechazarlas de inmediato. Sin burst, cualquier petición que supere la tasa recibe un 429. Con burst, Nginx retiene las peticiones excedentes en una cola y las libera a la tasa base.
| Parámetro | Peticiones inmediatas | Peticiones en cola | Rechazadas | Caso de uso |
|---|---|---|---|---|
burst=0 (defecto) |
1 por intervalo | Ninguna | Todo sobre la tasa | Límites de API estrictos |
burst=5 |
1 por intervalo | Hasta 5, liberadas a tasa base | Más de burst+1 | Envíos de formularios |
burst=5 nodelay |
Hasta 6 a la vez | Ninguna en cola, pero los slots de burst se recargan a tasa base | Más de burst+1 hasta recarga | Páginas de login, tráfico general |
burst=20 delay=10 |
Hasta 11 a la vez | Peticiones 12-21 throttled a tasa base | Más de burst+1 | APIs con picos ocasionales |
Con burst=5 (sin nodelay), si llegan 6 peticiones simultáneamente, la petición 1 se procesa al instante. Las peticiones 2-6 se encolan y se liberan una por intervalo (cada 100 ms a 10r/s). La última petición en cola espera 500 ms. Esto añade latencia pero nunca descarta ráfagas legítimas.
Con burst=5 nodelay, las 6 peticiones se procesan al instante. Pero los 5 slots de burst tardan 500 ms en recargarse. Si llegan 6 peticiones más 200 ms después, solo se han recargado 3 slots, así que 3 peticiones excedentes se rechazan.
Con burst=20 delay=10, las primeras 11 peticiones (1 base + 10 umbral de delay) se procesan sin espera. Las peticiones 12-21 se frenan a la tasa base. Todo lo que supere 21 se rechaza. Este modo híbrido funciona bien para APIs que reciben picos periódicos de clientes batch legítimos.
¿Cómo limito la tasa de diferentes endpoints por separado?
Define zonas separadas con claves diferentes para aplicar límites independientes. El ejemplo anterior ya usa tres zonas. También puedes limitar por ruta URI usando $uri como clave:
# Rate limiting por URI: limita el total de peticiones a cada URI única
limit_req_zone $uri zone=per_uri:10m rate=50r/s;
Esto es útil cuando ciertos endpoints (como una página de búsqueda o una función de exportación) necesitan throttling global independientemente del cliente que los llame.
Para rate limiting basado en clave de API, usa map para extraer la clave de un header:
map $http_x_api_key $api_key_limit {
default $binary_remote_addr;
"~^.+$" $http_x_api_key;
}
limit_req_zone $api_key_limit zone=api_keyed:10m rate=100r/s;
Si el cliente envía un header X-API-Key, el rate limit se basa en esa clave. Si no, recurre a limitación por IP.
¿Cómo limito conexiones con limit_conn?
Mientras limit_req controla la tasa de peticiones, limit_conn limita el número de conexiones simultáneas por cliente. Funciona bien contra ataques slowloris y abuso de descargas.
# En el bloque http
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_status 429;
limit_conn_log_level warn;
# En un bloque server o location
server {
# Máx. 20 conexiones simultáneas por IP
limit_conn addr 20;
location /downloads/ {
# Máx. 2 descargas simultáneas por IP
limit_conn addr 2;
limit_rate 1m; # Limitar ancho de banda a 1 MB/s por conexión
}
}
Nota para HTTP/2 y HTTP/3: cada petición concurrente cuenta como una conexión separada. Un navegador cargando una página con 30 recursos a través de una única conexión HTTP/2 cuenta como 30 conexiones para limit_conn. Establece el límite más alto que para HTTP/1.1.
limit_conn y limit_req son complementarios. Usa ambos. limit_req frena las ráfagas de peticiones. limit_conn frena las inundaciones de conexiones.
¿Cómo devuelvo una página de error 429 personalizada?
Por defecto, las peticiones limitadas reciben una página de error genérica. Una página 429 personalizada puede incluir un header Retry-After y un mensaje legible.
Crea la página de error:
sudo mkdir -p /var/www/error
sudo nano /var/www/error/429.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>429 - Too Many Requests</title>
<style>
body { font-family: system-ui, sans-serif; text-align: center; padding: 5rem 1rem; }
h1 { font-size: 2rem; }
p { color: #555; }
</style>
</head>
<body>
<h1>429 - Too Many Requests</h1>
<p>You have exceeded the request limit. Wait a moment and try again.</p>
</body>
</html>
sudo chmod 644 /var/www/error/429.html
Añade la página de error y el header Retry-After a tu bloque server:
server {
#... directivas de rate limiting...
error_page 429 /429.html;
location = /429.html {
root /var/www/error;
internal;
add_header Retry-After 5 always;
}
}
La directiva internal impide el acceso directo a la página de error. La palabra clave always en add_header asegura que el header se envíe incluso en respuestas de error. El valor de Retry-After (en segundos) indica a los clientes bien comportados cuándo reintentar.
¿Cómo pruebo los rate limits de forma segura con dry_run?
Activa limit_req_dry_run on para simular el rate limiting sin rechazar peticiones. Nginx registra lo que habría hecho, pero todas las peticiones pasan. Esta opción está disponible desde Nginx 1.17.1.
server {
limit_req zone=general burst=20 nodelay;
limit_req_dry_run on; # Solo registrar, no aplicar
# Añadir el estado de rate limiting a los logs de acceso
#...
}
Añade $limit_req_status a tu formato de log para rastrear eventos de dry run en los logs de acceso:
# En el bloque http
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rate_limit=$limit_req_status';
# En el bloque server
access_log /var/log/nginx/access.log ratelimit;
El flujo de dry_run:
- Añade
limit_req_dry_run on;a tu configuración - Recarga Nginx
- Genera tráfico de prueba (ver la sección de pruebas más abajo)
- Revisa el log de errores buscando entradas de dry run:
sudo grep "dry run" /var/log/nginx/error.log
2026/03/19 14:22:31 [warn] 1234#1234: *567 limiting requests, dry run, excess: 1.532 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
El nivel de log aquí es [warn] por la directiva limit_req_log_level warn. Asegúrate de que tu directiva error_log incluya el nivel warn o inferior, o estos mensajes no aparecerán. La configuración de producción con error_log /var/log/nginx/example.error.log warn; se encarga de esto.
- Revisa los logs de acceso buscando la variable
$limit_req_status:
sudo grep "REJECTED_DRY_RUN\|DELAYED_DRY_RUN" /var/log/nginx/access.log
La variable $limit_req_status devuelve uno de estos valores: PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN o REJECTED_DRY_RUN.
- Cuando las tasas se vean correctas, elimina la línea
limit_req_dry_runy recarga.
¿Cómo incluyo IPs de confianza en una lista blanca?
Usa un bloque geo para excluir sistemas de monitorización, load balancers o las IPs de tu oficina del rate limiting:
# En el bloque http
geo $limit {
default 1;
10.0.0.0/8 0; # Red interna
192.168.0.0/16 0; # Red interna
203.0.113.10 0; # Servidor de monitorización
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=general:10m rate=10r/s;
Cuando $limit_key es una cadena vacía, Nginx omite el rate limiting para esa petición. Las IPs que coinciden con el bloque geo obtienen $limit = 0, que se traduce en una clave vacía.
Si prefieres que las IPs de confianza tengan una tasa más alta en vez de no tener límite:
limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=trusted:1m rate=100r/s;
server {
limit_req zone=general burst=20 nodelay;
limit_req zone=trusted burst=100 nodelay;
}
Todas las IPs coinciden con trusted, pero solo las IPs no listadas coinciden con general. Se aplica el límite más restrictivo, así que las IPs de confianza quedan a 100 r/s mientras el resto obtiene 10 r/s.
¿Cómo bloqueo a los reincidentes con fail2ban?
El rate limiting rechaza peticiones individuales, pero los atacantes persistentes siguen volviendo. fail2ban monitoriza el log de errores de Nginx y bloquea IPs a nivel de firewall tras violaciones repetidas.
Instala fail2ban si aún no lo has hecho:
sudo apt update && sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-03-19 14:30:00 UTC; 2s ago
fail2ban incluye un filtro integrado nginx-limit-req. La regex del filtro coincide con líneas como:
limiting requests, excess: 1.532 by zone "general", client: 203.0.113.50
Crea la configuración del jail. Nunca edites archivos .conf directamente; usa overrides .local:
sudo nano /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 600
Esto bloquea una IP durante 10 minutos tras 10 violaciones de rate limiting en 60 segundos.
Para bloqueos escalonados, añade un segundo jail en el mismo archivo:
[nginx-limit-req-repeat]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 30
findtime = 3600
bantime = 86400
El primer jail atrapa ráfagas cortas (10 hits en un minuto = bloqueo de 10 minutos). El segundo atrapa a los reincidentes (30 hits en una hora = bloqueo de 24 horas).
Reinicia fail2ban y comprueba el estado del jail:
sudo systemctl restart fail2ban
sudo fail2ban-client status nginx-limit-req
En Debian 12 y sistemas anteriores, la salida se ve así:
Status for the jail: nginx-limit-req
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- File list: /var/log/nginx/error.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
En Ubuntu 24.04, fail2ban usa por defecto el backend de journal de systemd (backend = auto resuelve a systemd). La salida muestra Journal matches: en lugar de File list::
Status for the jail: nginx-limit-req
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=nginx.service + _COMM=nginx
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Ambos backends funcionan. El backend de journal lee los mismos mensajes de log de Nginx a través de systemd. Si prefieres monitorización basada en archivos, añade backend = pyinotify a la sección del jail.
Para desbloquear manualmente una IP durante las pruebas:
sudo fail2ban-client set nginx-limit-req unbanip 203.0.113.50
El log del jail está en:
sudo journalctl -u fail2ban -f
¿Cómo verifico que el rate limiting funciona?
Prueba desde una máquina externa al servidor. Nunca pruebes desde localhost porque 127.0.0.1 puede estar en tu lista blanca.
Prueba rápida con un bucle curl:
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
done
Con una tasa de 10r/s y burst=20 nodelay, las primeras 21 peticiones devuelven 200. Una vez agotado el burst, las respuestas cambian a 429.
Prueba de carga con wrk:
sudo apt install -y wrk
wrk -t2 -c10 -d10s https://example.com/
Running 10s test @ https://example.com/
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 5.23ms 2.11ms 28.44ms 75.32%
Req/Sec 1.02k 121.33 1.34k 68.00%
20384 requests in 10.01s, 15.22MB read
Non-2xx or 3xx responses: 18241
Requests/sec: 2036.36
Transfer/sec: 1.52MB
El contador Non-2xx or 3xx responses muestra cuántas peticiones limitó Nginx. Aquí, 18.241 de 20.384 peticiones recibieron un 429.
Revisa el log de errores durante la prueba:
sudo tail -f /var/log/nginx/error.log
2026/03/19 14:45:12 [warn] 1234#1234: *890 limiting requests, excess: 9.876 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
El valor excess muestra cuánto excedió el límite la petición. Valores más altos indican tráfico más agresivo.
Configuración de producción completa
La configuración completa de rate limiting combinando las tres capas:
# /etc/nginx/conf.d/rate-limiting.conf
# --- Lista blanca ---
geo $limit {
default 1;
10.0.0.0/8 0;
192.168.0.0/16 0;
# Añade aquí tus IPs de monitorización / oficina
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
# --- Zonas de tasa de peticiones ---
limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $limit_key zone=login:10m rate=1r/s;
limit_req_zone $limit_key zone=api:10m rate=30r/s;
# --- Zona de conexiones ---
limit_conn_zone $binary_remote_addr zone=addr:10m;
# --- Códigos de respuesta y registro ---
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
limit_conn_log_level warn;
# --- Log de acceso con estado de rate limiting ---
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rate_limit=$limit_req_status';
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/example.access.log ratelimit;
error_log /var/log/nginx/example.error.log warn;
# Límites globales
limit_req zone=general burst=20 nodelay;
limit_conn addr 30;
# Página 429 personalizada
error_page 429 /429.html;
location = /429.html {
root /var/www/error;
internal;
add_header Retry-After 5 always;
}
location /login {
limit_req zone=login burst=3 nodelay;
limit_conn addr 5;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api burst=50 delay=30;
limit_conn addr 20;
proxy_pass http://127.0.0.1:3000;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}
# /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/example.error.log
maxretry = 10
findtime = 60
bantime = 600
[nginx-limit-req-repeat]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/example.error.log
maxretry = 30
findtime = 3600
bantime = 86400
Tras escribir todos los archivos de configuración:
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart fail2ban
Para una configuración de seguridad más amplia incluyendo headers, TLS y otras medidas de hardening, consulta.
Referencia de directivas
| Directiva | Contexto | Defecto | Desde |
|---|---|---|---|
limit_req_zone |
http | - | 0.7.21 |
limit_req |
http, server, location | - | 0.7.21 |
limit_req_status |
http, server, location | 503 | 1.3.15 |
limit_req_log_level |
http, server, location | error | 0.8.18 |
limit_req_dry_run |
http, server, location | off | 1.17.1 |
limit_conn_zone |
http | - | 1.1.8 |
limit_conn |
http, server, location | - | 0.7.21 |
limit_conn_status |
http, server, location | 503 | 1.3.15 |
limit_conn_log_level |
http, server, location | error | 0.8.18 |
limit_conn_dry_run |
http, server, location | off | 1.17.6 |
Solución de problemas
El rate limiting no funciona en absoluto: Comprueba que limit_req_zone está en el bloque http, no dentro de un bloque server. Las zonas deben definirse antes de ser referenciadas. Si tu configuración usa directivas include, asegúrate de que el archivo de zonas se incluye antes de los bloques server.
Usuarios legítimos reciben 429: Reduce la rate, aumenta el burst, o cambia de sin-burst a burst=N nodelay. Usa el modo dry_run para medir los patrones de tráfico reales antes de fijar límites. Comprueba si el multiplexado HTTP/2 está inflando los contadores de limit_conn.
fail2ban no bloquea: Confirma que el logpath coincide con la ubicación real de los logs de error de Nginx. Verifica que limit_req_log_level está configurado como warn o error (el valor por defecto). Comprueba que el jail está activo con sudo fail2ban-client status nginx-limit-req. Si pruebas desde localhost, ten en cuenta que fail2ban ignora la IP del propio servidor por defecto (ignoreself = true). Prueba desde una máquina externa.
Los mensajes de rate limit no aparecen en el log de errores: La directiva error_log tiene nivel error por defecto, lo que filtra los mensajes de nivel warn de limit_req_log_level warn. Configura error_log /var/log/nginx/error.log warn; en tu bloque server para ver los eventos de rate limiting.
Memoria de zona agotada: Una zona de 10m alberga unos 160.000 estados IPv4. Si ves could not allocate node en los logs, aumenta el tamaño de la zona. Monitoriza el uso de la zona con $limit_req_status en tus logs de acceso.
Detrás de un load balancer o CDN: Si Nginx solo ve la IP del load balancer, el rate limiting se aplica a esa única IP. Usa $http_x_forwarded_for o $realip_remote_addr como clave de zona en lugar de $binary_remote_addr. También debes configurar set_real_ip_from con el rango de IPs del load balancer. Confía en X-Forwarded-For solo desde proxies conocidos, ya que los clientes pueden falsificarlo.
Los logs son tu herramienta principal de depuración:
# Seguir eventos de rate limiting en tiempo real
sudo tail -f /var/log/nginx/error.log | grep "limiting"
# Comprobar acciones de fail2ban
sudo journalctl -u fail2ban -f
Copyright 2026 Virtua.Cloud. Todos los derechos reservados. Este contenido es una obra original del equipo de Virtua.Cloud. La reproducción, republicación o redistribución sin permiso escrito está prohibida.
¿Listo para probarlo?
Despliega tu propio servidor en segundos. Linux, Windows o FreeBSD.
Ver planes VPS