Patrones de rate limiting para APIs que escalan
Token buckets, sliding windows y rate limiting distribuido — patrones prácticos para proteger tu API sin degradar la experiencia de los usuarios legítimos.

Toda API pública necesita rate limiting. Sin él, un solo cliente mal comportado puede saturar tu base de datos, agotar tu presupuesto de cómputo, o tumbar el servicio entero. Pero un rate limiting ingenuo — un simple contador que se reinicia cada minuto — crea efectos de acantilado y penaliza el tráfico legítimo en ráfagas.
Ventana fija: simple pero defectuosa
El enfoque más simple: contar solicitudes por ventana de tiempo. Cuando el contador supera el límite, rechazar.
// ❌ Fixed window — boundary burst problem
async function fixedWindowLimit(
key: string,
limit: number,
windowMs: number,
): Promise<boolean> {
const window = Math.floor(Date.now() / windowMs);
const counterKey = `ratelimit:${key}:${window}`;
const count = await redis.incr(counterKey);
if (count === 1) await redis.pexpire(counterKey, windowMs);
return count <= limit;
}
// Problem: at 11:59:59, a client sends 100 requests (under the limit).
// At 12:00:01, they send another 100. Both pass — 200 requests in 2 seconds.El problema del límite implica que un cliente puede efectivamente duplicar su rate limit sincronizando solicitudes a través de los bordes de la ventana.
Log de ventana deslizante
Registra los timestamps individuales de cada solicitud y cuenta cuántos caen dentro de la ventana deslizante. Más preciso, pero usa más memoria.
async function slidingWindowLog(
key: string,
limit: number,
windowMs: number,
): Promise<boolean> {
const now = Date.now();
const windowStart = now - windowMs;
const sortedSetKey = `ratelimit:${key}`;
// Remove expired entries and add current request atomically
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(sortedSetKey, 0, windowStart);
pipeline.zadd(sortedSetKey, now, `${now}:${Math.random()}`);
pipeline.zcard(sortedSetKey);
pipeline.pexpire(sortedSetKey, windowMs);
const results = await pipeline.exec();
const count = results![2][1] as number;
return count <= limit;
}Esto elimina el problema del límite, pero almacena una entrada por solicitud. Para endpoints de alto tráfico, eso es mucha memoria de Redis.
Token bucket: la mejor opción por defecto
Los token buckets permiten ráfagas mientras hacen cumplir una tasa promedio. Los tokens se agregan a una tasa constante; cada solicitud consume un token. Cuando el bucket está vacío, las solicitudes se rechazan.
// ✅ Token bucket — allows bursts, enforces average rate
async function tokenBucket(
key: string,
capacity: number,
refillRate: number, // tokens per second
): Promise<{ allowed: boolean; remaining: number }> {
const bucketKey = `bucket:${key}`;
const now = Date.now();
// Lua script for atomicity
const script = `
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'lastRefill')
local tokens = tonumber(bucket[1]) or tonumber(ARGV[1])
local lastRefill = tonumber(bucket[2]) or tonumber(ARGV[3])
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local elapsed = (now - lastRefill) / 1000
tokens = math.min(capacity, tokens + elapsed * refillRate)
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'lastRefill', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(capacity / refillRate) * 1000)
return {allowed, math.floor(tokens)}
`;
const [allowed, remaining] = (await redis.eval(
script,
1,
bucketKey,
capacity,
refillRate,
now,
)) as [number, number];
return { allowed: allowed === 1, remaining };
}Un bucket con capacidad 100 y tasa de recarga de 10/segundo permite una ráfaga de 100 solicitudes, y luego sostiene 10/segundo. Esto se ajusta mejor a los patrones de uso reales que las ventanas fijas.
Elegir una estrategia
| Algoritmo | Manejo de ráfagas | Memoria | Precisión | Complejidad |
|---|---|---|---|---|
| Ventana fija | Malo (límite) | Baja | Baja | Simple |
| Log de ventana deslizante | Bueno | Alta | Alta | Media |
| Contador de ventana deslizante | Bueno | Baja | Media | Media |
| Token bucket | Excelente | Baja | Alta | Media |
Token bucket es la opción correcta por defecto para la mayoría de las APIs. La ventana fija es aceptable para servicios internos donde la precisión no importa.
Headers de respuesta y manejo de 429
Comunica siempre el estado del rate limit mediante headers. Los clientes bien comportados los usan para autolimitarse.
function rateLimitResponse(
res: Response,
limit: number,
remaining: number,
resetAt: number,
) {
res.setHeader("X-RateLimit-Limit", limit);
res.setHeader("X-RateLimit-Remaining", Math.max(0, remaining));
res.setHeader("X-RateLimit-Reset", Math.ceil(resetAt / 1000));
if (remaining < 0) {
res.setHeader("Retry-After", Math.ceil((resetAt - Date.now()) / 1000));
return res.status(429).json({
error: {
code: "RATE_LIMITED",
message:
"Too many requests. Please retry after the Retry-After period.",
},
});
}
}Rate limits escalonados
Distintos endpoints tienen distintos perfiles de costo. Un endpoint de búsqueda que golpea un índice de texto completo es más costoso que una verificación de estado.
const rateLimits = {
"GET /api/status": { capacity: 1000, refillRate: 100 },
"GET /api/search": { capacity: 20, refillRate: 5 },
"POST /api/orders": { capacity: 10, refillRate: 2 },
"POST /api/auth/login": { capacity: 5, refillRate: 1 },
} as const;
function getRateLimit(method: string, path: string) {
const key = `${method} ${path}`;
return rateLimits[key] ?? { capacity: 100, refillRate: 20 };
}Los endpoints de autenticación merecen los límites más estrictos — son el blanco principal de los ataques de fuerza bruta.
Puntos clave
- Token bucket es la mejor opción por defecto — maneja las ráfagas de forma natural mientras hace cumplir tasas promedio
- Las ventanas fijas tienen un problema de límite — los clientes pueden duplicar su tasa efectiva en los bordes de la ventana
- Usa scripts de Lua en Redis para operaciones de rate limiting atómicas de múltiples pasos
- Siempre devuelve headers de rate limit —
X-RateLimit-RemainingyRetry-Afterayudan a los clientes a autolimitarse - Escalona los rate limits según el costo del endpoint — las operaciones costosas reciben límites más estrictos
- Los endpoints de autenticación necesitan los límites más estrictos — son el blanco principal de la fuerza bruta


