Objetivos de aprendizaje
Después de leer este artículo podrás:
- Entender en qué se diferencian la ventana fija, la ventana deslizante y el token bucket
- Aplicar rate limiting por IP en una API Express y devolver 429 con Retry-After
- Probar el límite con curl y manejar las respuestas 429 en un cliente
En esta página
- ¿Qué hace el rate limiting?
- ¿Qué algoritmo de rate limiting conviene usar?
- ¿Cómo agrego rate limiting a una API Express?
- ¿Cómo pruebo que el límite funciona?
- ¿Qué debe contener una respuesta 429?
- ¿Por qué el rate limiting falla con varias instancias?
- ¿Basta el rate limiting para detener ataques?
- Cómo hacerlo en Square Cloud
- Preguntas frecuentes
Toda API pública acaba encontrándose con un cliente que envía demasiado: un script atrapado en un bucle, un scraper o alguien intentando adivinar contraseñas. Sin un límite, un solo cliente puede agotar la CPU y las conexiones a la base de datos que necesitan todos los demás.
El rate limiting es la protección contra eso. A continuación conocerás los algoritmos más comunes, agregarás un limitador a una API Node.js, lo probarás y verás cómo debe reaccionar un cliente cuando recibe un rechazo. El texto asume que ya conoces lo básico de una API REST.
¿Qué hace el rate limiting?#
Un rate limiter se coloca delante de los handlers de tus rutas. En cada petición elige una clave (normalmente la IP, la clave de API o el ID del usuario), incrementa un contador para esa clave y lo compara con el límite de la ventana actual. Por debajo del límite, la petición pasa. Por encima, el servidor responde de inmediato con 429 Too Many Requests y nunca ejecuta el handler.
Te protege de tres cosas:
- Abuso y fuerza bruta: intentos en las rutas de login, registro y restablecimiento de contraseña.
- Accidentes: un cliente con un bug que reintenta en un bucle cerrado.
- Uso injusto: un usuario intensivo que ralentiza a todos los demás, y facturas inesperadas en rutas costosas (consultas a la base, llamadas a IA, envío de correos).
El rate limiting no es autenticación ni un firewall. Controla con qué frecuencia puede llamar una clave conocida, no quién tiene permiso para llamar.
¿Qué algoritmo de rate limiting conviene usar?#
| Algoritmo | Cómo funciona | Bueno para | Ojo con |
|---|---|---|---|
| Ventana fija | Cuenta las peticiones por ventana de calendario (por ejemplo, por minuto) y reinicia en el cambio | Límites simples, el predeterminado en la mayoría de las bibliotecas | Un cliente puede enviar el límite dos veces seguidas alrededor del cambio (final de una ventana más inicio de la siguiente) |
| Ventana deslizante | Cuenta las peticiones de los últimos N segundos, medidos desde ahora | Límites suaves y justos | Más memoria y trabajo por petición |
| Token bucket | Un cubo se rellena a un ritmo constante; cada petición gasta una ficha | APIs que permiten ráfagas cortas pero limitan el promedio | Necesita dos números: ritmo de recarga y tamaño del cubo |
| Límite de concurrencia | Limita las peticiones simultáneas en curso | Rutas lentas o costosas | No limita el volumen total |
Para la mayoría de las APIs, una ventana fija basta para empezar. Pasa a una ventana deslizante o a un token bucket cuando la ráfaga en el cambio de ventana de verdad te afecte.
¿Cómo agrego rate limiting a una API Express?#
Necesitas una versión reciente de Node.js y una aplicación Express. Los ejemplos usan express-rate-limit 8. Los encabezados draft-7 que se usan abajo requieren Express 4.11 o posterior.
npm install express express-rate-limitCrea server.mjs:
import express from "express";
import { rateLimit } from "express-rate-limit";
const app = express();
// General limit: 5 requests per minute per IP (low on purpose, to make testing easy)
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 5,
standardHeaders: "draft-7",
legacyHeaders: false,
message: { error: "too_many_requests" },
});
// Stricter limit for a sensitive route: 10 attempts per 15 minutes
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10,
standardHeaders: "draft-7",
legacyHeaders: false,
});
app.use("/api", apiLimiter);
app.post("/api/login", loginLimiter, (req, res) => res.json({ ok: true }));
app.get("/api/hello", (req, res) => res.json({ hello: "world" }));
app.listen(process.env.PORT || 3000, "0.0.0.0");Qué hace cada opción:
windowMses la duración de la ventana en milisegundos. El valor por defecto es 60000 (un minuto).limites el número máximo de peticiones por clave en la ventana. El valor por defecto es 5. El nombre antiguomaxsigue funcionando.standardHeadersenvía los encabezados de respuestaRateLimityRateLimit-Policy. Viene desactivado por defecto."draft-7"usa un único encabezadoRateLimitcombinado.legacyHeaders: falsedesactiva los antiguos encabezadosX-RateLimit-*, que vienen activados por defecto.messagees el cuerpo de la respuesta 429. Puede ser un string o un objeto JSON.
Una petición a /api/login pasa por los dos limitadores, primero el general y luego el más estricto, así que cada intento de login cuenta para ambos.
¿Y la IP detrás de un proxy?#
Por defecto, la clave es la IP del cliente que Express expone en req.ip. Detrás de un proxy inverso o un balanceador de carga, esa es la dirección del proxy, a menos que le indiques a Express que confíe en el encabezado X-Forwarded-For. Si no, todos los usuarios comparten un solo contador y quedan bloqueados juntos.
app.set("trust proxy", 1); // one proxy in front of the appUn número indica cuántos saltos de proxy son de confianza. Evita true salvo que el último proxy sobrescriba X-Forwarded-For; de lo contrario, un cliente puede falsificar el encabezado y elegir su propia clave. Revisa qué devuelve req.ip una vez hecho el deploy y ajusta el valor al número real de proxies. Para entender el contexto, lee sobre el proxy inverso.
¿Cómo pruebo que el límite funciona?#
Arranca el servidor y llama a la misma ruta seis veces:
PORT=3000 node server.mjs &
for i in 1 2 3 4 5 6; do curl -s -i http://localhost:3000/api/hello | grep -iE "^HTTP|ratelimit|retry|error"; doneLas cinco primeras llamadas devuelven 200 OK y un encabezado RateLimit que va descontando. La sexta devuelve:
HTTP/1.1 429 Too Many Requests
RateLimit-Policy: 5;w=60
RateLimit: limit=5, remaining=0, reset=59
Retry-After: 59
{"error":"too_many_requests"}Pasado más o menos un minuto, el contador se reinicia y la ruta vuelve a responder 200. Si nunca ves un 429, revisa el orden: app.use("/api", apiLimiter) tiene que ir antes de las rutas que protege.
¿Qué debe contener una respuesta 429?#
- Estado 429, definido en el RFC 6585. No uses 403 ni 500.
Retry-After, que el RFC 9110 define como un número de segundos o una fecha HTTP. El servidor puede omitirlo, pero los clientes dependen de él para saber cuándo reintentar.express-rate-limitlo define por ti.- Un cuerpo legible por máquina, como
{"error":"too_many_requests"}, para que los clientes no tengan que interpretar texto. - Encabezados
RateLimityRateLimit-Policy, para que los clientes bien programados bajen el ritmo antes de chocar con el límite. Vienen de un borrador del IETF todavía en curso (draft-ietf-httpapi-ratelimit-headers), no de un estándar final, así que el formato exacto aún puede cambiar. La salidadraft-7de arriba sigue una revisión anterior del borrador; las revisiones más nuevas escriben los valores de otra forma, así que consulta la documentación de la biblioteca antes de interpretar estos encabezados. Los nombres antiguosX-RateLimit-*son comunes, pero informales.
Nunca pongas secretos ni detalles internos en el cuerpo del error.
¿Cómo debe manejar un cliente el 429?#
El cliente debe detenerse y esperar en lugar de reintentar de inmediato. Esta función respeta Retry-After cuando existe y, si no, recurre a un backoff exponencial:
async function fetchWithRetry(url, options = {}, maxRetries = 4) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429 || attempt >= maxRetries) return res;
const retryAfter = Number(res.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 2 ** attempt * 500;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}Esta versión lee Retry-After solo como segundos. El encabezado también puede ser una fecha HTTP, así que un cliente de producción debe manejar las dos formas.
¿Por qué el rate limiting falla con varias instancias?#
El almacenamiento por defecto guarda los contadores en la memoria de un solo proceso Node.js. Eso trae dos problemas:
- Un reinicio pone todos los contadores a cero. Un cliente puede esperar a un deploy y empezar de nuevo.
- Varias instancias cuentan por separado. Con tres instancias y
limit: 100, un cliente puede enviar hasta 300 peticiones por ventana, según la instancia que elija el balanceador. Lo mismo pasa con el móduloclusterde Node.
La solución es un almacenamiento compartido. Redis encaja bien porque INCR es atómico y EXPIRE elimina los contadores viejos por sí solo, que es el patrón descrito en la documentación de Redis. Con rate-limit-redis 6 y el cliente redis:
npm install rate-limit-redis redisimport { rateLimit } from "express-rate-limit";
import { RedisStore } from "rate-limit-redis";
import { createClient } from "redis";
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 60,
standardHeaders: "draft-7",
legacyHeaders: false,
store: new RedisStore({
sendCommand: (...args) => client.sendCommand(args),
}),
});Guarda la URL de Redis en una variable de entorno, nunca en el código. Si ejecutas una sola instancia y aceptas que los contadores vuelvan a cero al reiniciar, el almacenamiento en memoria sirve.
¿Basta el rate limiting para detener ataques?#
No. Un limitador dentro de tu aplicación solo protege las capas que están detrás de él. Cada petición rechazada sigue costando una conexión, el parsing y la ejecución de un middleware. Contra inundaciones volumétricas necesitas filtrar el tráfico antes de que llegue a tu código, que es lo que hace la protección contra DDoS. Mantén el limitador de la aplicación para garantizar un uso justo y frenar abusos en rutas concretas como el login, y revísalo junto con el resto de la checklist de API en producción.
Cómo hacerlo en Square Cloud#
Square Cloud es hosting self-service: tú haces el deploy de la API y escribes el limitador, como se mostró arriba. El código corre en el runtime de Node.js, en un contenedor aislado siempre encendido, sin cold start ni modo de suspensión. Aun así, los contadores en memoria vuelven a cero cada vez que el proceso se reinicia. Las aplicaciones web empiezan en 512 MB de RAM, y el panel te deja definir variables de entorno por aplicación, como REDIS_URL.
Todos los planes ya incluyen protección DDoS (L3, L4 y L7) y un WAF delante de tu aplicación, así que el limitador de tu código puede centrarse en límites por ruta y por clave. Si ejecutas más de una instancia y necesitas contadores compartidos, el hosting de Redis está disponible desde el plan Standard, en el mismo datacenter que tus aplicaciones.
Consulta los detalles y los planes en hosting de APIs en Square Cloud.
Hosting de APIs
APIs en contenedores siempre encendidos, sin cold start
Preguntas frecuentes
¿Qué código HTTP indica rate limit?
429 Too Many Requests, definido en el RFC 6585. El servidor puede añadir el encabezado Retry-After para indicar cuánto debe esperar el cliente antes de reintentar.
¿Cuál es un buen límite de peticiones para una API?
No hay un número universal. Parte de lo que necesita un cliente normal (por ejemplo, 60 peticiones por minuto en una ruta pública de lectura) y pon límites mucho más estrictos en las rutas de login, registro y restablecimiento de contraseña.
¿El rate limiting detiene ataques DDoS?
Solo los pequeños. Un limitador dentro de tu aplicación sigue gastando CPU y memoria en cada petición rechazada, así que las inundaciones grandes deben absorberse antes, en el borde de la red, sin llegar a tu código.
¿Limito por dirección IP o por clave de API?
Usa la clave de API o el ID del usuario cuando la petición está autenticada, y la IP en las rutas anónimas. Un límite por IP puede bloquear a muchos usuarios detrás de una misma dirección, como una oficina o un operador móvil.
¿Por qué mi rate limiter no funciona con varias instancias?
El contador por defecto en memoria vive dentro de un solo proceso. Con varias instancias o después de un reinicio, cada una cuenta por separado, así que necesitas un almacenamiento compartido como Redis.
Más en APIs
Ver los 15 artículos- Cómo hospedar una aplicación Node.jsPara hospedar una aplicación Node.js, prepara el package.json, lee el puerto y los secretos del entorno, elige una versión LTS y ejecútala con node. Paso a paso.
- Cómo hospedar FastAPI, Flask o DjangoPara hospedar FastAPI, Flask o Django, ejecuta la aplicación con un servidor de producción (Uvicorn o Gunicorn) en 0.0.0.0. Comandos, ajustes de Django y errores.
- Cómo hospedar una app o script de PythonPara hospedar una app o script de Python, fija la versión, aísla las dependencias en un venv, lee los secretos del entorno y maneja SIGTERM. Guía paso a paso.
- Qué es un cron job y cómo programar tareasUn cron job es una tarea que corre sola en horarios definidos por una expresión de 5 campos. Mira sintaxis, ejemplos, errores comunes y cómo programar en Node.js.