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:

AlgoritmoIdeaDónde lo encuentras
Leaky bucketLas peticiones salen a ritmo constante; las que desbordan la cola se rechazanNginx (limit_req)
Token bucketSe acumulan fichas a ritmo fijo; cada petición gasta una y permite ráfagas hasta el tamaño del cuboMuchas librerías y API gateways
Ventana fijaContador por intervalo (por ejemplo, 100 por minuto) que se reinicia al cambiar de minutoImplementaciones simples con Redis INCR + EXPIRE
Ventana deslizanteTasa calculada sobre los últimos N segundos, sin saltos al cambiar de intervaloHAProxy (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

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_addr ocupa 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_key usa la cabecera X-API-Key tal 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/8 por las redes que de verdad quieras excluir, o elimina la línea.

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=20 permite que un cliente supere el ritmo de 10 r/s con hasta 20 peticiones extra en cola. Por encima de eso, 429.
  • nodelay atiende las peticiones de la ráfaga inmediatamente en vez de espaciarlas; el hueco se libera al ritmo definido. Sin nodelay, Nginx retrasa las peticiones para ajustarlas al ritmo, lo que aumenta la latencia percibida.
  • Cuando hay varias directivas limit_req en 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/login es 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

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

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 (INCR con EXPIRE para 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_from con los rangos del proxy y real_ip_header X-Forwarded-For (o CF-Connecting-IP).
  • Devuelve 503 en vez de 429: falta limit_req_status 429;. El valor por defecto es 503.
  • zero size shared memory zone al validar: una directiva limit_req usa una zona que no se ha declarado con limit_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_pass y no un return.

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.