--- title: "Cómo limitar peticiones a una API con Nginx y Redis" description: "Limita la tasa de peticiones de tu API con limit_req de Nginx por IP y por API key, y comparte los contadores entre varios servidores con OpenResty y Redis." category: "API y Microservicios" language: es source: https://cubepath.com/es/docs/api-y-microservicios/limitacion-de-tasa-api-con-nginx-y-redis updated: 2026-09-25 --- # Cómo limitar peticiones a una API con Nginx y Redis Limitar la tasa de peticiones protege una API frente a abusos, scrapers y picos que tumbarían el backend, y reparte la capacidad de forma justa entre clientes. En este tutorial configurarás en Ubuntu 24.04 el módulo `limit_req` de Nginx para limitar por IP, aplicar límites más estrictos al login y dar un cupo mayor a los clientes con API key válida, con respuestas `429` en JSON y un modo de prueba para ajustar los valores con tráfico real. Después verás cómo compartir los contadores entre varios servidores con OpenResty y Redis. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. - Un usuario no root con privilegios `sudo`. - Nginx instalado (`sudo apt install nginx`) y el puerto 80 permitido en el firewall. - Tu API escuchando en `127.0.0.1:8000`. Si todavía no la tienes, el paso 1 crea un backend de prueba. - Para la parte con Redis (pasos 6 a 8): uno o varios servidores que actúen como proxy de la misma API. ## Cómo funciona limit_req `limit_req` usa el algoritmo *leaky bucket*: cada clave (una IP, una API key) tiene un cubo que se vacía a la velocidad `rate`. Las peticiones que llegan cuando el cubo está lleno se rechazan. Tres parámetros controlan el comportamiento: | Parámetro | Dónde | Qué hace | | --- | --- | --- | | `rate` | `limit_req_zone` | Velocidad sostenida permitida, en peticiones por segundo (`r/s`) o por minuto (`r/m`) | | `burst` | `limit_req` | Cuántas peticiones por encima de `rate` se aceptan de golpe antes de rechazar | | `nodelay` | `limit_req` | Atiende la ráfaga al momento en lugar de espaciarla a la velocidad `rate` | Sin `nodelay`, Nginx retrasa las peticiones de la ráfaga para ajustarlas a `rate`, lo que añade latencia. Para una API suele ser preferible `nodelay`: el cliente recibe respuesta inmediata o un `429`. Los contadores viven en memoria compartida de cada servidor Nginx. Una zona de 1 MB guarda unos 16.000 estados de IP, así que 10 MB bastan para la mayoría de APIs. ## Paso 1: Crear un backend de prueba Para probar los límites necesitas un backend real detrás de `proxy_pass`: una directiva `return` en la misma `location` se ejecuta antes que `limit_req` y se saltaría el límite. Si ya tienes tu API en `127.0.0.1:8000`, omite este paso. Si no, crea un servidor de prueba en Nginx que responda JSON: ```bash sudo nano /etc/nginx/sites-available/backend-demo ``` ```nginx server { listen 127.0.0.1:8000; location / { default_type application/json; return 200 '{"ok":true}\n'; } } ``` ```bash sudo ln -s /etc/nginx/sites-available/backend-demo /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx curl -s http://127.0.0.1:8000/ ``` ```output {"ok":true} ``` ## Paso 2: Definir las zonas de limitación Las zonas se declaran en el contexto `http`. En Ubuntu, los archivos de `/etc/nginx/conf.d/` se incluyen en ese contexto, así que crea uno para toda la configuración de límites: ```bash sudo nano /etc/nginx/conf.d/ratelimit.conf ``` ```nginx # API keys válidas: devuelve el nombre del cliente o "" si la key no existe map $http_x_api_key $api_client { default ""; include /etc/nginx/api-keys.map; } # Clave para peticiones anónimas: la IP solo si no hay API key válida map $api_client $limit_anon { "" $binary_remote_addr; default ""; } limit_req_zone $limit_anon zone=api_anon:10m rate=5r/s; limit_req_zone $api_client zone=api_clients:10m rate=50r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; limit_req_status 429; limit_req_log_level warn; log_format ratelimit '$remote_addr [$time_local] "$request" $status ' 'client=$api_client limit=$limit_req_status'; ``` Qué hace cada parte: - Las peticiones con una clave vacía no se cuentan en una zona. Por eso cada petición cae en una sola de las dos zonas de la API: si trae una API key válida, `$limit_anon` queda vacío y cuenta en `api_clients` (50 r/s por cliente); si no, `$api_client` queda vacío y cuenta en `api_anon` (5 r/s por IP). - La API key se compara con una lista de keys conocidas. Si usaras directamente `$http_x_api_key` como clave, cualquiera podría saltarse el límite enviando una key inventada distinta en cada petición. - `$binary_remote_addr` ocupa 4 bytes (16 en IPv6), menos que la IP en texto, y ahorra memoria en la zona. - `limit_req_status 429` sustituye el `503` que Nginx devuelve por defecto, que los clientes interpretarían como una caída del servicio. - La variable `$limit_req_status` del formato de log indica el resultado de cada petición: `PASSED`, `DELAYED`, `REJECTED` o sus variantes `_DRY_RUN`. Crea el archivo de API keys, con una línea por cliente. Usa keys largas y aleatorias; puedes generarlas con `openssl rand -hex 24`: ```bash sudo nano /etc/nginx/api-keys.map ``` ```nginx "your_api_key_for_acme" "acme"; "your_api_key_for_globex" "globex"; ``` ```bash sudo chmod 640 /etc/nginx/api-keys.map sudo chown root:www-data /etc/nginx/api-keys.map ``` ## Paso 3: Aplicar los límites a la API Crea el sitio que publica la API y aplica las zonas en cada `location`: ```bash sudo nano /etc/nginx/sites-available/api ``` ```nginx server { listen 80; listen [::]:80; server_name your_domain; access_log /var/log/nginx/api-ratelimit.log ratelimit; # Respuesta 429 en JSON error_page 429 = @ratelimited; location @ratelimited { default_type application/json; add_header Retry-After 1 always; return 429 '{"detail":"Too many requests"}\n'; } location /api/ { limit_req zone=api_anon burst=10 nodelay; limit_req zone=api_clients burst=100 nodelay; proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location = /api/login { limit_req zone=login burst=3 nodelay; proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` Nginx elige la `location` más específica, así que `/api/login` solo aplica la zona `login` (5 intentos por minuto por IP, con un margen de 3), que frena ataques de fuerza bruta contra las contraseñas. El resto de `/api/` aplica las dos zonas de la API. Activa el sitio y recarga Nginx: ```bash sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` ## Paso 4: Probar los límites Lanza 20 peticiones seguidas sin API key. Las 11 primeras pasan (1 más la ráfaga de 10) y el resto recibe `429`: ```bash for i in $(seq 1 20); do curl -s -o /dev/null -w '%{http_code} ' -H 'Host: your_domain' http://127.0.0.1/api/; done; echo ``` ```output 200 200 200 200 200 200 200 200 200 200 200 429 429 429 429 429 429 429 429 429 ``` Repite con una API key válida. El límite de 50 r/s con ráfaga de 100 no se alcanza: ```bash for i in $(seq 1 20); do curl -s -o /dev/null -w '%{http_code} ' -H 'Host: your_domain' -H 'X-API-Key: your_api_key_for_acme' http://127.0.0.1/api/; done; echo ``` ```output 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 ``` Una key inventada no da acceso a ese cupo: cuenta como anónima y comparte el límite de la IP. Comprueba ahora el login y el cuerpo de la respuesta `429`: ```bash for i in $(seq 1 6); do curl -s -o /dev/null -w '%{http_code} ' -H 'Host: your_domain' http://127.0.0.1/api/login; done; echo curl -si -H 'Host: your_domain' http://127.0.0.1/api/login ``` ```output 200 200 200 200 429 429 HTTP/1.1 429 Too Many Requests Server: nginx/1.24.0 (Ubuntu) Content-Type: application/json Content-Length: 31 Connection: keep-alive Retry-After: 1 {"detail":"Too many requests"} ``` ## Paso 5: Ajustar los valores con el modo de prueba y los logs Poner límites demasiado bajos en producción corta a clientes legítimos. Antes de activarlos, puedes medirlos con `limit_req_dry_run`: Nginx calcula los límites y los registra, pero deja pasar todas las peticiones. Añádelo en la `location` que quieras evaluar: ```nginx location /api/ { limit_req zone=api_anon burst=10 nodelay; limit_req zone=api_clients burst=100 nodelay; limit_req_dry_run on; # ... } ``` Recarga Nginx, deja pasar tráfico real durante unas horas y cuenta cuántas peticiones se habrían rechazado, y de qué IPs o clientes: ```bash sudo grep -c 'limit=REJECTED_DRY_RUN' /var/log/nginx/api-ratelimit.log sudo grep 'limit=REJECTED' /var/log/nginx/api-ratelimit.log | awk '{print $1, $(NF-1)}' | sort | uniq -c | sort -rn | head ``` ```output 37 25 198.51.100.23 client= 12 203.0.113.9 client=acme ``` Si un cliente legítimo aparece en la lista, sube su `burst` o el `rate` de su zona. Cuando los números te convenzan, quita `limit_req_dry_run on;` y recarga. En modo normal, el log de errores también registra cada rechazo con la zona que lo provocó: ```bash sudo grep 'limiting requests' /var/log/nginx/error.log | tail -n 2 ``` ```output 2026/09/25 10:20:14 [warn] 2211#2211: *1893 limiting requests, excess: 10.520 by zone "api_anon", client: 198.51.100.23, server: your_domain, request: "GET /api/items HTTP/1.1", host: "your_domain" ``` > Importante: si hay un balanceador o CDN delante de Nginx, todas las peticiones llegan con su IP y compartirían el mismo límite. Configura el módulo `realip` (`set_real_ip_from` y `real_ip_header`) con los rangos de ese proxy para que `$binary_remote_addr` sea la IP real del cliente. ## Paso 6: Instalar OpenResty y Redis para límites compartidos Las zonas de `limit_req` son locales a cada servidor. Si tu API está detrás de varios servidores Nginx, un cliente obtiene el límite multiplicado por el número de servidores. Para un límite global, los contadores deben estar en un almacén compartido como Redis, y consultarlo desde Nginx requiere Lua. OpenResty es una distribución de Nginx que incluye LuaJIT y el cliente `lua-resty-redis`. OpenResty ocupa los mismos puertos que Nginx, así que en cada servidor proxy sustituye a Nginx. Detén y desactiva Nginx: ```bash sudo systemctl disable --now nginx ``` Añade el repositorio oficial de OpenResty. En servidores ARM, usa `https://openresty.org/package/arm64/ubuntu` como URL: ```bash sudo apt install ca-certificates curl gnupg sudo install -d -m 0755 /etc/apt/keyrings curl -fsSL https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/openresty.gpg echo "deb [signed-by=/etc/apt/keyrings/openresty.gpg] https://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list sudo apt update sudo apt install openresty openresty -v ``` ```output nginx version: openresty/1.31.1.1 ``` En el servidor que alojará los contadores (puede ser uno de los proxies o una máquina aparte), instala Redis: ```bash sudo apt install redis-server ``` Edita su configuración para exigir contraseña y, si los proxies están en otras máquinas, escuchar también en la IP privada: ```bash sudo nano /etc/redis/redis.conf ``` ```ini bind 127.0.0.1 -::1 your_private_ip requirepass your_redis_password maxmemory 256mb maxmemory-policy volatile-ttl ``` Todos los contadores tienen caducidad, así que `volatile-ttl` descarta primero los que están a punto de expirar si Redis llega al límite de memoria. Reinicia Redis, permite el acceso solo desde tu red privada y comprueba que responde: ```bash sudo systemctl restart redis-server sudo ufw allow from 10.0.0.0/24 to any port 6379 proto tcp redis-cli -a 'your_redis_password' --no-auth-warning ping ``` ```output PONG ``` Sustituye `10.0.0.0/24` por el rango de tu red privada. Nunca expongas Redis a Internet. ## Paso 7: Escribir el limitador en Lua El limitador usa una ventana fija: cada clave tiene un contador por ventana de `window` segundos, que se incrementa con `INCR` y caduca con `EXPIRE`. Ambas operaciones van en un script que Redis ejecuta de forma atómica, para que dos proxies no se pisen. Si Redis no responde, el limitador deja pasar la petición y lo registra en el log: es preferible perder la limitación unos segundos que tumbar la API. Crea el directorio del módulo y el archivo: ```bash sudo mkdir -p /usr/local/openresty/nginx/conf/lua sudo nano /usr/local/openresty/nginx/conf/lua/ratelimit.lua ``` ```lua local redis = require "resty.redis" local _M = {} -- INCR y EXPIRE atómicos: el contador vive exactamente una ventana local SCRIPT = [[ local current = redis.call("INCR", KEYS[1]) if current == 1 then redis.call("EXPIRE", KEYS[1], ARGV[1]) end return current ]] function _M.limit(opts) local id = ngx.var.http_x_api_key if not id or id == "" then id = ngx.var.remote_addr end local now = ngx.time() local bucket = math.floor(now / opts.window) local reset = (bucket + 1) * opts.window - now -- md5 evita guardar la API key en claro y acota la longitud de la clave local key = "rl:" .. ngx.md5(id) .. ":" .. bucket local red = redis:new() red:set_timeouts(100, 100, 100) local ok, err = red:connect(opts.host, opts.port) if not ok then ngx.log(ngx.ERR, "ratelimit: no se puede conectar a Redis: ", err) return end if opts.password and red:get_reused_times() == 0 then ok, err = red:auth(opts.password) if not ok then ngx.log(ngx.ERR, "ratelimit: fallo de autenticación en Redis: ", err) return end end local current current, err = red:eval(SCRIPT, 1, key, opts.window) if not current then ngx.log(ngx.ERR, "ratelimit: error en Redis: ", err) return end red:set_keepalive(10000, 100) local remaining = opts.limit - current if remaining < 0 then remaining = 0 end ngx.ctx.ratelimit = { limit = opts.limit, remaining = remaining, reset = reset } if current > opts.limit then ngx.status = 429 ngx.header["Content-Type"] = "application/json" ngx.header["Retry-After"] = reset ngx.say('{"detail":"Too many requests"}') return ngx.exit(ngx.HTTP_OK) end end function _M.headers() local rl = ngx.ctx.ratelimit if rl then ngx.header["X-RateLimit-Limit"] = rl.limit ngx.header["X-RateLimit-Remaining"] = rl.remaining ngx.header["X-RateLimit-Reset"] = rl.reset end end return _M ``` Detalles importantes: - `set_keepalive` devuelve la conexión a un pool por worker, así que no se abre una conexión TCP nueva a Redis en cada petición. `get_reused_times() == 0` evita repetir `AUTH` en las conexiones reutilizadas. - `ngx.exit(ngx.HTTP_OK)` tras enviar el cuerpo termina la petición con el estado `429` fijado en `ngx.status`. En la fase de acceso no uses `ngx.exit(ngx.OK)`, que dejaría continuar la petición. - Una ventana fija permite, en el peor caso, hasta el doble del límite en el cambio de una ventana a la siguiente. Para límites por minuto u hora de una API suele ser aceptable; si no lo es, combínalo con `limit_req` local para cortar las ráfagas. ## Paso 8: Configurar OpenResty y probar el límite global El `nginx.conf` de OpenResty está en `/usr/local/openresty/nginx/conf/`. Sustitúyelo por una configuración mínima que cargue el módulo y los sitios de un directorio propio: ```bash sudo cp /usr/local/openresty/nginx/conf/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf.orig sudo mkdir -p /usr/local/openresty/nginx/conf/sites sudo nano /usr/local/openresty/nginx/conf/nginx.conf ``` ```nginx worker_processes auto; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; lua_package_path "/usr/local/openresty/nginx/conf/lua/?.lua;;"; include sites/*.conf; } ``` Crea el sitio de la API. El límite global es de 100 peticiones por minuto por API key o IP: ```bash sudo nano /usr/local/openresty/nginx/conf/sites/api.conf ``` ```nginx server { listen 80; listen [::]:80; server_name your_domain; location /api/ { access_by_lua_block { require("ratelimit").limit({ host = "your_redis_ip", port = 6379, password = "your_redis_password", limit = 100, window = 60, }) } header_filter_by_lua_block { require("ratelimit").headers() } proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` Usa `127.0.0.1` como `your_redis_ip` si Redis está en el mismo servidor. El archivo contiene la contraseña de Redis, así que restringe sus permisos, valida la configuración y arranca OpenResty: ```bash sudo chmod 600 /usr/local/openresty/nginx/conf/sites/api.conf sudo openresty -t sudo systemctl enable --now openresty ``` ```output nginx: the configuration file /usr/local/openresty/nginx/conf/nginx.conf syntax is ok nginx: configuration file /usr/local/openresty/nginx/conf/nginx.conf test is successful ``` Comprueba las cabeceras de una petición: ```bash curl -si -H 'Host: your_domain' http://127.0.0.1/api/ | grep -i ratelimit ``` ```output X-RateLimit-Limit: 100 X-RateLimit-Remaining: 99 X-RateLimit-Reset: 42 ``` Repite la configuración en el resto de proxies apuntando al mismo Redis. Si envías peticiones alternando entre dos proxies, verás que `X-RateLimit-Remaining` baja de forma continua en ambos, porque comparten el contador. Puedes ver los contadores activos en Redis: ```bash redis-cli -a 'your_redis_password' --no-auth-warning --scan --pattern 'rl:*' ``` ```output rl:0b6f1a3c9e2d4f5a8b7c6d5e4f3a2b1c:29809316 ``` Si Redis cae, las peticiones siguen pasando y el log de errores de OpenResty lo refleja: ```bash sudo tail -n 2 /usr/local/openresty/nginx/logs/error.log ``` ## Solución de problemas **Todos los clientes reciben 429 a la vez.** Todas las peticiones llegan con la misma IP, normalmente la de un balanceador o CDN. Configura `realip` como se indica en el paso 5. **Nginx devuelve 503 en lugar de 429.** Falta `limit_req_status 429;` o está en un contexto que no aplica a esa `location`. **Los límites no se aplican.** Comprueba que la `location` hace `proxy_pass` o sirve archivos: una directiva `return` o `rewrite ... last` en la misma `location` se ejecuta antes que `limit_req` y que `access_by_lua_block`. **`openresty -t` falla con `module 'ratelimit' not found`.** La ruta de `lua_package_path` no coincide con la ubicación del archivo `.lua`. Debe terminar en `?.lua` y apuntar al directorio que contiene `ratelimit.lua`. **`ratelimit: no se puede conectar a Redis: timeout` en el log.** El proxy no llega a Redis: revisa `bind` en `redis.conf`, la regla de UFW y que la IP sea la privada. ## Conclusión Has limitado tu API con `limit_req` de Nginx por IP, con un cupo mayor para los clientes con API key válida y un límite estricto en el login, has aprendido a calibrar los valores con el modo de prueba y has compartido un límite global entre varios proxies con OpenResty y Redis. Como siguientes pasos, puedes asignar límites distintos por plan añadiendo zonas por tipo de cliente en el `map`, enviar el log `api-ratelimit.log` a tu sistema de monitorización para alertar de picos de rechazos o proteger con Fail2ban las IPs que acumulan `429` en el login.