Limitación de Tasa (Rate Limiting)
Cómo implementar rate limiting en APIs usando token bucket, sliding window y fixed window en Python, JavaScript y Java.
Visión General
El rate limiting controla cuántas peticiones puede hacer un cliente a tu API en una ventana de tiempo dada. Previene abusos, asegura una asignación justa de recursos y protege servicios downstream de sobrecarga — y es una de las pocas defensas que funciona a cualquier escala, desde una API de juguete de un solo proceso hasta una distribuida globalmente.
Esta receta implementa los tres algoritmos que de verdad te vas a encontrar: token bucket (Python, en memoria), fixed window (JavaScript + Redis, distribuido) y sliding window (Java, en memoria). La tabla comparativa bajo el código te dice cuál elegir, y la sección de distribución cubre qué cambia cuando tu API corre en más de un servidor.
Cuándo Usarlo
Usa esta receta cuando:
- Proteges APIs públicas de abuso o DDoS
- Aplicas límites de uso por nivel (planes gratis vs de pago)
- Previenes ataques de fuerza bruta en endpoints de autenticación — consulta JWT Authentication para el lado de auth
- Gestionas capacidad para operaciones intensivas en recursos
- Implementas políticas de uso justo entre usuarios
No limites a ojo: elige el límite a partir de datos de uso reales (p95 de clientes legítimos más margen), no de un número redondo que “suena seguro”.
Solución
Python (Token Bucket)
Token bucket es la opción más común para APIs que toleran ráfagas: los tokens se rellenan a ritmo constante y un cliente puede gastarlos más rápido de lo que se rellenan hasta que el cubo se vacía.
import time
from threading import Lock
class TokenBucket:
def __init__(self, capacity: int, refill_rate: float):
self.capacity = capacity
self.tokens = capacity
self.refill_rate = refill_rate
self.last_refill = time.time()
self.lock = Lock()
def allow(self) -> bool:
with self.lock:
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
self.last_refill = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
bucket = TokenBucket(capacity=10, refill_rate=1)
print(bucket.allow()) # True
JavaScript (Fixed Window con Redis)
Fixed window es el algoritmo más simple que funciona entre instancias: un contador por clave y ventana, incrementado atómicamente en Redis. Es el punto de partida correcto para la mayoría de APIs distribuidas — consulta Rate Limiting con Redis para la versión endurecida para producción.
const redis = require('redis');
const client = redis.createClient();
async function rateLimit(key, limit, windowSeconds) {
const windowKey = `${key}:${Math.floor(Date.now() / 1000 / windowSeconds)}`;
const current = await client.incr(windowKey);
if (current === 1) {
await client.expire(windowKey, windowSeconds);
}
return current <= limit;
}
// Middleware de Express — ver la receta de middleware para el patrón
async function limiter(req, res, next) {
const key = `ratelimit:${req.ip}`;
const allowed = await rateLimit(key, 100, 60);
if (!allowed) return res.status(429).json({ error: 'Too many requests' });
next();
}
Java (Sliding Window)
El log de sliding window guarda un timestamp por petición y elimina el problema de ráfagas en el borde de la ventana, a costa de almacenar cada timestamp.
import java.util.concurrent.*;
public class SlidingWindow {
private final int capacity;
private final long windowMs;
private final ConcurrentLinkedDeque<Long> timestamps = new ConcurrentLinkedDeque<>();
public SlidingWindow(int capacity, long windowMs) {
this.capacity = capacity;
this.windowMs = windowMs;
}
public synchronized boolean allow() {
long now = System.currentTimeMillis();
while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMs) {
timestamps.pollFirst();
}
if (timestamps.size() < capacity) {
timestamps.addLast(now);
return true;
}
return false;
}
}
Comparación de Algoritmos
| Algoritmo | Ventajas | Desventajas | Ideal para |
|---|---|---|---|
| Fixed Window | Simple, poca memoria | Ráfaga en el borde de ventana (hasta 2× el límite) | Protección básica, la mayoría de APIs |
| Sliding Window | Tasa suave, sin ráfagas de borde | Más memoria/computación | Control preciso de tasa |
| Token Bucket | Permite ráfagas hasta la capacidad | Más difícil de implementar bien | APIs con tolerancia a ráfagas |
| Leaky Bucket | Tasa de salida estrictamente constante | Encola o descarta el exceso | Proteger downstreams lentos |
Elegir un Algoritmo
La tabla se comprime en tres preguntas. ¿Importan las ráfagas? Si un cliente necesita legítimamente 10 peticiones de golpe una vez por minuto, token bucket las absorbe donde fixed window las rechaza. ¿Vale la precisión la memoria? El log de sliding window guarda un timestamp por petición — bien para cientos de peticiones por ventana, derrochador para millones; el contador de sliding window (ponderando la ventana anterior) es el punto medio habitual. ¿Dónde vive el estado? En memoria solo es correcto cuando un proceso sirve el tráfico o los límites son por instancia; en el momento en que existe una segunda instancia, el contador debe moverse a Redis o al edge.
Para la mayoría de APIs la respuesta es: fixed window en Redis primero, token bucket cuando lleguen quejas por ráfagas, sliding window solo cuando alguien pueda nombrar el incidente de ráfaga-de-borde que está resolviendo.
Rate Limiting Distribuido
El ejemplo de JavaScript de arriba es el caso distribuido: Redis guarda el contador, así que cada instancia ve el mismo número. Tres detalles deciden si sobrevive a producción:
- ¿Fallar abierto o cerrado? Si Redis cae, ¿permites todo (abierto — gana disponibilidad, el abuso se cuela durante la caída) o rechazas todo (cerrado — gana protección, la API muere con Redis)? La mayoría de APIs falla abierto para lectura y cerrado en endpoints de login/pagos. Decídelo explícitamente; no dejes que el manejador de excepciones por defecto decida por ti.
- Deriva de reloj. La matemática de ventanas usa la hora del servidor; el skew de NTP entre app servers desplaza ligeramente los bordes de ventana. Es inofensivo en ventanas de minutos y solo importa con precisión de sub-segundo.
- Diseño de claves.
ratelimit:{ip}castiga a todos detrás de una NAT o proxy corporativo. Prefiere API key o user ID, con IP como fallback solo para tráfico anónimo — y da a los usuarios autenticados un nivel superior.
Límites por Endpoint
Un único límite global trata igual un GET /status barato que un POST /reports/export caro. En la práctica quieres presupuestos ponderados o por endpoint: lecturas baratas con techo alto, escrituras caras con techo bajo, y endpoints de autenticación (/login, /password-reset) con el límite más estricto de todos — son los que los atacantes martillean de verdad. Impleméntalo como un mapa de ruta a límite, no como N limitadores separados, para que añadir un endpoint sea un cambio de config y no un middleware nuevo.
const LIMITS = {
'POST:/login': { limit: 5, window: 60 }, // objetivo de fuerza bruta
'POST:/reports/export': { limit: 10, window: 3600 }, // caro
default: { limit: 100, window: 60 },
};
async function limiter(req, res, next) {
const cfg = LIMITS[`${req.method}:${req.route?.path}`] ?? LIMITS.default;
const key = `ratelimit:${req.user?.id ?? req.ip}:${req.method}:${req.route?.path}`;
const allowed = await rateLimit(key, cfg.limit, cfg.window);
if (!allowed) {
res.set('Retry-After', String(cfg.window));
return res.status(429).json({ error: 'Too many requests' });
}
next();
}
Probar tu Limitador
Los limitadores fallan en silencio — o dejan pasar de más o bloquean usuarios reales, y ninguno de los dos lanza una excepción. Tres tests cubren los modos de fallo: ráfaga hasta el límite y confirma que el 429 llega con Retry-After; espera a que la ventana expire y confirma que el contador se resetea (en tests, encoge la ventana a 1-2 segundos en lugar de dormir un minuto); y mata Redis a mitad de test para confirmar que tu decisión de fail-open/fail-closed es la que realmente tomaste. Una prueba de carga que supere el límite también verifica que el limitador en sí no es el cuello de botella — el check debería añadir milisegundos de un dígito, no un round trip extra completo por petición.
Devolver 429 Correctamente
Un rechazo por rate limit sigue siendo una respuesta de API y merece el mismo cuidado que un 200. Devuelve 429 Too Many Requests con cabecera Retry-After (segundos hasta reintentar, según RFC 6585), e idealmente los campos RateLimit del IETF — RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset — para que los clientes bien educados se auto-limiten en lugar de descubrir el límite tropezando con él. Incluye el límite en tu documentación de API; un límite que nadie conoce se lee como inestabilidad, no como política.
Lo Que Funciona
- Devuelve 429 con
Retry-After— los clientes que saben cuándo reintentar dejan de martillear; los que no, se bloquean igualmente - Usa Redis para límites distribuidos — los contadores en memoria duplican silenciosamente el límite por cada instancia que añadas
- Diferencia por cliente — API key o user ID, no solo IP; las NATs compartidas castigan a usuarios inocentes
- Nivela por confianza — usuarios autenticados más que anónimos; planes de pago más que gratis
- Registra los eventos de límite — un pico de 429s es o un ataque o un límite demasiado estricto; conviene saber cuál
- Dile a los clientes el límite — las cabeceras
RateLimit-*convierten el backoff reactivo en ritmo proactivo
Errores Comunes
- Limitar solo por IP — las NATs corporativas y los gateways de operadoras concentran miles de usuarios detrás de una dirección
- Contadores en memoria detrás de un load balancer — N instancias significa N veces el límite pretendido
- Sin plan para cuando cae Redis — una excepción sin manejar mata la API (cerrado por accidente) o desactiva el limitador (abierto por accidente)
- Límites elegidos por intuición — 100 req/min bloquea un dashboard legítimo que abre 40 llamadas; mide el uso real de clientes primero
- 429 sin
Retry-After— los clientes reintentan inmediatamente, empeorando el problema que defiendes
Solución de Problemas
- Usuarios legítimos chocan con los límites: comprueba si estás clavando por IP detrás de una NAT/proxy — cambia a API key o user ID, y sube el nivel anónimo solo tras medir.
- Picos de 429 al inicio del minuto: los clientes sincronizaron sus reintentos con el borde de la ventana — añade
Retry-Aftercon jitter, o pasa a sliding window para que el borde desaparezca. - El límite se duplicó tras escalar: contador en memoria + segunda instancia — mueve el contador a Redis; cada proceso estaba aplicando su propia copia.
- Redis caído y la API rechazando todo: el limitador falló cerrado sin querer — envuelve la llamada a Redis en un timeout y decide fail-open vs fail-closed por clase de endpoint.
- El limitador añade latencia visible: el round trip a Redis está en el hot path — usa un script Lua para hacer check+increment en un solo viaje, o permite una pequeña ráfaga local y reconcilia asíncronamente.
Lecturas Adicionales
- RFC 6585 — Additional HTTP Status Codes — define
429 Too Many Requestsy la semántica deRetry-After - Campos de cabecera RateLimit del IETF — el estándar
RateLimit-Limit/Remaining/Reset - Documentación de Redis INCR — la primitiva detrás de los contadores de fixed window
- Relacionado en este sitio: Rate Limiting con Redis para patrones Redis de producción, token bucket con Redis para la variante con script Lua, y la receta de API gateway para enforcement en el edge.
Preguntas frecuentes
¿Debería limitar en el edge o en la aplicación?
En ambos. Los límites de edge/CDN (Cloudflare, AWS WAF) absorben DDoS y floods de credential stuffing antes de que lleguen a tu infraestructura. Los límites a nivel de aplicación aplican la lógica de negocio — cuotas por plan, endpoints caros, uso justo. El edge solo no puede ver la identidad del usuario; la app sola quema cómputo en tráfico que podrías haber descartado antes.
¿Qué código de estado HTTP debería devolver al limitar?
429 Too Many Requests, definido por el RFC 6585, con cabecera Retry-After indicando los segundos hasta que el cliente puede reintentar. Los campos RateLimit-* del IETF además te permiten anunciar el límite, la cuota restante y el tiempo de reset para que los clientes se auto-limiten.
¿Cómo limito sin Redis en un sistema distribuido?
Las sticky sessions fijan cada cliente a una instancia (frágil — un rebalanceo resetea su presupuesto). Un contador en la base de datos compartida funciona pero añade latencia y carga de escritura. La respuesta honesta es que el rate limiting distribuido quiere un almacén compartido rápido; Redis es el estándar porque INCR + EXPIRE es atómico y barato.
¿Qué algoritmo elijo para una API REST típica?
Fixed window en Redis. Son dos comandos de Redis, suficientemente correcto para la mayoría de límites, y la ráfaga de borde que permite (2× en el filo de la ventana) raramente importa en ventanas de minutos. Pasa a token bucket cuando los usuarios se quejen de rechazos en ráfaga, o a sliding window cuando la precisión realmente cueste dinero.
¿Cómo gestiono la limitación para usuarios autenticados vs anónimos?
Dos niveles con claves distintas: ratelimit:user:{id} con límite generoso para autenticados, ratelimit:ip:{ip} con uno más estricto para anónimos. Nunca apliques límites anónimos a usuarios autenticados — una IP de oficina compartida no debería estrangular la cuenta de un cliente que paga.
¿El rate limiting protege contra DDoS?
Contra floods pequeños y abuso, sí — capea el throughput por cliente. Contra DDoS volumétrico ayuda solo indirectamente: el limitador en sí cuesta una llamada a Redis por petición, y a escala de DDoS real el propio check se convierte en el cuello de botella. La defensa volumétrica vive en la capa CDN/edge; el rate limiting es la segunda línea.
Recursos Relacionados
Implementar Rate Limiting de APIs con Redis
Protege APIs del abuso con limitadores token bucket y ventana deslizante respaldados por Redis, con ráfagas controladas y coordinación distribuida
RecipeRate Limiting contra Abuso y Ataques de Fuerza Bruta
Rate limiting como control de seguridad contra fuerza bruta, credential stuffing, scraping y DDoS L7: límites por capas, ajuste por endpoint y gestión del 429.
RecipeRate limiting con Redis y algoritmo Token Bucket
Implementa un rate limiter distribuido con token bucket usando operaciones atomicas de Redis para throttling de API entre multiples instancias
RecipeRate limiting distribuido con FastAPI y Redis
Implementa rate limiting distribuido en FastAPI usando algoritmos de sliding window y token bucket con Redis para límites por usuario, IP y endpoint.
RecipeMiddleware: Interceptores HTTP
Cómo implementar middleware de request/response para logging, auth y manejo de errores en Python, JavaScript y Java.
RecipeDiseñar un API Gateway Escalable para Microservicios
Construí un gateway de API que enrute requests, maneje autenticación, rate limiting, caching y traducción de protocolos entre clientes y microservicios backend.