La limitación de peticiones (rate limiting) controla cuántas solicitudes puede hacer cada cliente a tu API en un intervalo de tiempo. Sirve para frenar abusos y fuerza bruta, repartir la capacidad entre clientes y evitar que un pico de tráfico tumbe el backend. En este tutorial configurarás Nginx en Ubuntu 24.04 como proxy inverso delante de una API con límites por IP, por clave de API y por endpoint, devolviendo 429 Too Many Requests en JSON. Al final verás cómo hacer lo mismo con las stick tables de HAProxy.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. - Una API escuchando en local, en este tutorial en
127.0.0.1:8080. Si todavía no tienes una, el paso 1 levanta un backend de prueba. - Un nombre de dominio apuntando al servidor (en los ejemplos,
your_domain) o, para pruebas, la IP del servidor. - El puerto 80 abierto en el firewall.
Cómo funcionan los algoritmos de limitación
Antes de configurar nada conviene saber qué algoritmo usa cada herramienta, porque determina cómo se comportan las ráfagas:
| Algoritmo | Idea | Dónde lo encuentras |
|---|---|---|
| Leaky bucket | Las peticiones salen a ritmo constante; las que desbordan la cola se rechazan | Nginx (limit_req) |
| Token bucket | Se acumulan fichas a ritmo fijo; cada petición gasta una y permite ráfagas hasta el tamaño del cubo | Muchas librerías y API gateways |
| Ventana fija | Contador por intervalo (por ejemplo, 100 por minuto) que se reinicia al cambiar de minuto | Implementaciones simples con Redis INCR + EXPIRE |
| Ventana deslizante | Tasa calculada sobre los últimos N segundos, sin saltos al cambiar de intervalo | HAProxy (http_req_rate) |
En Nginx, rate fija el ritmo del cubo y burst el tamaño de la cola. En HAProxy defines una ventana (por ejemplo, 10 segundos) y comparas la tasa medida con un umbral.
Paso 1: Instalar Nginx y preparar un backend de prueba
Instala Nginx desde los repositorios de Ubuntu:
sudo apt update
sudo apt install nginx
Comprueba que el servicio está activo:
systemctl status nginx --no-pager
● 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)
Si ya tienes tu API en 127.0.0.1:8080, salta al paso 2. Si no, abre una segunda sesión SSH y levanta un servidor HTTP mínimo que haga de backend mientras sigues el tutorial:
mkdir -p ~/api-test/api && echo '{"status":"ok"}' > ~/api-test/api/index.html
cd ~/api-test && python3 -m http.server 8080 --bind 127.0.0.1
Importanteel límite de Nginx se aplica en la fase de acceso de la petición. Una directiva
return 200se ejecuta antes, así que una location que solo hacereturnnunca se limitará. Prueba siempre contra unproxy_passreal.
Paso 2: Definir las zonas de limitación
Las zonas son memoria compartida donde Nginx guarda el estado de cada clave (IP, clave de API...). Se declaran en el contexto http. En Ubuntu, todo lo que pongas en /etc/nginx/conf.d/ se incluye dentro de ese bloque, así que crea un archivo dedicado:
sudo nano /etc/nginx/conf.d/ratelimit.conf
# IPs que no se limitan (monitorización, oficina, otros servidores propios)
geo $rl_exempt {
default 0;
127.0.0.1 1;
10.0.0.0/8 1;
}
# Clave de limitación por IP: vacía para las IPs exentas.
# Nginx no contabiliza las peticiones cuya clave está vacía.
map $rl_exempt $rl_ip_key {
0 $binary_remote_addr;
1 "";
}
# 10 peticiones por segundo por IP
limit_req_zone $rl_ip_key zone=api_ip:10m rate=10r/s;
# 5 peticiones por minuto por IP para el login
limit_req_zone $rl_ip_key zone=api_login:10m rate=5r/m;
# 600 peticiones por minuto por clave de API (cabecera X-API-Key)
limit_req_zone $http_x_api_key zone=api_key:10m rate=600r/m;
# Máximo de conexiones simultáneas por IP
limit_conn_zone $rl_ip_key zone=api_conn:10m;
# Código de respuesta cuando se supera un límite
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
Detalles importantes:
$binary_remote_addrocupa 4 bytes por IPv4 y 16 por IPv6, frente a los hasta 39 de$remote_addr. Una zona de 10 MB guarda unas 160.000 claves.- La zona
api_keyusa la cabeceraX-API-Keytal cual. Las peticiones sin cabecera tienen la clave vacía y no cuentan en esa zona, pero sí en la de IP. - Sustituye
10.0.0.0/8por las redes que de verdad quieras excluir, o elimina la línea.
Notasi Nginx está detrás de otro proxy o balanceador,
$binary_remote_addrserá la IP del balanceador y todos los clientes compartirán el mismo límite. En ese caso configura el móduloreal_ip(set_real_ip_fromyreal_ip_header) antes de limitar.
Paso 3: Aplicar los límites en el virtual host
Crea el virtual host de la API:
sudo nano /etc/nginx/sites-available/api
upstream api_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
server_name your_domain;
# Respuesta JSON para las peticiones limitadas
error_page 429 = @ratelimited;
location @ratelimited {
default_type application/json;
add_header Retry-After 1 always;
return 429 '{"detail":"Too many requests"}\n';
}
# Límite general de la API
location /api/ {
limit_req zone=api_ip burst=20 nodelay;
limit_req zone=api_key burst=50 nodelay;
limit_conn api_conn 20;
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Endpoint sensible: límite mucho más estricto
location = /api/login {
limit_req zone=api_login burst=3 nodelay;
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Qué hace cada parámetro:
burst=20permite que un cliente supere el ritmo de 10 r/s con hasta 20 peticiones extra en cola. Por encima de eso,429.nodelayatiende las peticiones de la ráfaga inmediatamente en vez de espaciarlas; el hueco se libera al ritmo definido. Sinnodelay, Nginx retrasa las peticiones para ajustarlas al ritmo, lo que aumenta la latencia percibida.- Cuando hay varias directivas
limit_reqen la misma location, se aplican todas: una petición con clave de API debe respetar el límite de su IP y el de su clave. location = /api/logines una coincidencia exacta y gana a/api/, así que el login solo tiene su propio límite estricto.
Activa el sitio, desactiva el sitio por defecto, valida la sintaxis y recarga:
sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/api
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Consejoantes de activar límites en producción puedes añadir
limit_req_dry_run on;en la location. Nginx registra qué peticiones habría rechazado sin rechazarlas, así ajustasrateyburstcon tráfico real.
Paso 4: Probar los límites
Lanza 40 peticiones seguidas contra la API y cuenta los códigos de respuesta:
for i in $(seq 1 40); do curl -s -o /dev/null -w '%{http_code}\n' http://your_domain/api/; done | sort | uniq -c
Con rate=10r/s y burst=20, las primeras 21 aproximadamente pasan y el resto se rechazan:
21 200
19 429
Comprueba el cuerpo y las cabeceras de una respuesta limitada repitiendo el bucle y consultando una vez más:
curl -si http://your_domain/api/
HTTP/1.1 429 Too Many Requests
Server: nginx/1.24.0 (Ubuntu)
Content-Type: application/json
Retry-After: 1
{"detail":"Too many requests"}
Prueba el endpoint de login, que admite solo 4 peticiones seguidas (1 más la ráfaga de 3):
for i in $(seq 1 8); do curl -s -o /dev/null -w '%{http_code}\n' -X POST http://your_domain/api/login; done
Las cuatro primeras llegan al backend (con el servidor de prueba de Python verás 501, porque no implementa POST) y las cuatro siguientes reciben 429.
Por último, confirma que Nginx registra los rechazos en el log de errores:
sudo tail -n 3 /var/log/nginx/error.log
2026/09/25 10:12:41 [warn] 1234#1234: *57 limiting requests, excess: 20.410 by zone "api_ip", client: 203.0.113.10, server: your_domain, request: "GET /api/ HTTP/1.1", host: "your_domain"
Paso 5: Registrar el estado del límite en el log de acceso
La variable $limit_req_status indica si cada petición pasó (PASSED), se retrasó (DELAYED), se rechazó (REJECTED) o se habría rechazado en modo de prueba (REJECTED_DRY_RUN). Añádela a un formato de log propio en /etc/nginx/conf.d/ratelimit.conf:
log_format ratelimit '$remote_addr [$time_local] "$request" $status '
'key="$http_x_api_key" limit=$limit_req_status';
Y úsalo dentro del bloque server del virtual host:
access_log /var/log/nginx/api_access.log ratelimit;
Recarga y consulta qué IPs reciben más rechazos:
sudo nginx -t && sudo systemctl reload nginx
sudo grep 'limit=REJECTED' /var/log/nginx/api_access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
Advertenciasi tus clientes envían la clave de API en
X-API-Key, este formato la escribe en el log en claro. Si eso no es aceptable, quita el campokey=dellog_format.
Alternativa: limitación con HAProxy
HAProxy mide la tasa de peticiones con stick tables, que calculan una ventana deslizante por clave. Instálalo con:
sudo apt install haproxy
Edita la configuración y añade un frontend y un backend al final del archivo (si Nginx sigue escuchando en el puerto 80, detenlo o cambia el bind para esta prueba):
sudo nano /etc/haproxy/haproxy.cfg
frontend api
bind :80
mode http
# Tasa por IP en ventana de 10 segundos
stick-table type ip size 100k expire 30s store http_req_rate(10s)
http-request track-sc0 src
# Tasa por clave de API, en una tabla separada del backend auxiliar
http-request track-sc1 req.hdr(X-API-Key) table api_keys if { req.hdr(X-API-Key) -m found }
# Límites: 100 peticiones cada 10 s por IP, 500 cada 10 s por clave
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
http-request deny deny_status 429 if { sc_http_req_rate(1) gt 500 }
default_backend api_servers
backend api_keys
stick-table type string len 64 size 100k expire 60s store http_req_rate(10s)
backend api_servers
mode http
server api1 127.0.0.1:8080 check
Valida y recarga:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Puedes ver en directo el contenido de la tabla por IP a través del socket de administración, que Ubuntu habilita por defecto en /run/haproxy/admin.sock:
echo "show table api" | sudo socat stdio /run/haproxy/admin.sock
# table: api, type: ip, size:102400, used:1
0x55d0c1a2b3c0: key=203.0.113.10 use=0 exp=28512 shard=0 http_req_rate(10000)=37
Si socat no está instalado, instálalo con sudo apt install socat.
Limitación en varios servidores
Las zonas de Nginx y las stick tables de HAProxy viven en la memoria de cada servidor. Con tres balanceadores detrás de un DNS round robin, un cliente puede hacer hasta tres veces el límite. Hay tres soluciones habituales:
- Dividir el límite entre el número de nodos (sencillo e impreciso).
- En HAProxy, sincronizar las stick tables entre nodos con una sección
peers. - Aplicar el límite en la propia API con un contador compartido en Redis (
INCRconEXPIREpara ventana fija, o un sorted set para ventana deslizante) cuando necesites límites exactos por usuario o por plan.
Una estrategia combinada suele funcionar mejor: límites generosos por IP en el proxy para cortar floods, y límites de negocio por usuario o plan en la aplicación.
Solución de problemas
- Todos los clientes comparten el límite: Nginx ve la IP del balanceador o de Cloudflare. Configura
set_real_ip_fromcon los rangos del proxy yreal_ip_header X-Forwarded-For(oCF-Connecting-IP). - Devuelve 503 en vez de 429: falta
limit_req_status 429;. El valor por defecto es 503. zero size shared memory zoneal validar: una directivalimit_requsa una zona que no se ha declarado conlimit_req_zone, o hay un error tipográfico en el nombre.- El límite no se aplica nunca: comprueba que la petición cae en la location correcta y que esa location hace
proxy_passy no unreturn.
Conclusión
Has configurado límites por IP, por clave de API y por endpoint en Nginx, con respuestas 429 en JSON, una lista blanca de redes y registro del estado de cada petición, y has visto el equivalente con stick tables en HAProxy. Como siguientes pasos, activa HTTPS con Let's Encrypt en el virtual host, ajusta rate y burst con limit_req_dry_run sobre tráfico real y documenta los límites para los clientes de tu API.
