Implementar Rate Limiting para APIs y Aplicaciones Web
Cómo proteger APIs y endpoints web del abuso usando estrategias de rate limiting token bucket, sliding window y fixed window con implementaciones en Redis y memoria.
Visión general
El rate limiting es una técnica defensiva que controla cuántas peticiones puede realizar un cliente a una API o endpoint web dentro de una ventana de tiempo determinada. Sin rate limiting, un solo cliente que se comporta mal — ya sea malicioso o accidentalmente con bugs — puede agotar recursos del backend, privar a usuarios legítimos y desencadenar fallos en cascada a través de sistemas distribuidos.
El rate limiting útil se implementa en múltiples capas: API gateway (edge), middleware de aplicación (servicio) y base de datos (throttling de queries). Cada capa usa diferentes algoritmos adecuados a diferentes trade-offs. Token bucket permite ráfagas, sliding window proporciona precisión, y fixed window es simple pero vulnerable a avalanchas en los límites de la ventana. Aqui se explica como implementaciones desde in-memory de nodo único hasta limitación distribuida respaldada por Redis.
Cuándo usarlo
Usa esta receta cuando:
- Protegiendo APIs públicas del abuso, scraping o ataques de fuerza bruta
- Limitando operaciones costosas para prevenir sobrecarga del backend
- Haciendo cumplir límites de uso por tier para usuarios gratuitos vs pagados
- Previniendo fallos en cascada durante picos de tráfico o eventos DDoS
- Cumpliendo con requisitos de SLA para asignación justa de recursos
Solución
Token Bucket (Python / Redis)
import time
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def is_allowed(key: str, capacity: int, refill_rate: float) -> bool:
now = time.time()
pipe = r.pipeline()
pipe.hmget(key, ['tokens', 'last_refill'])
pipe.expire(key, 60)
result = pipe.execute()
tokens = float(result[0][0]) if result[0][0] else capacity
last_refill = float(result[0][1]) if result[0][1] else now
elapsed = now - last_refill
tokens = min(capacity, tokens + elapsed * refill_rate)
if tokens >= 1:
tokens -= 1
r.hmset(key, {'tokens': tokens, 'last_refill': now})
r.expire(key, 60)
return True
else:
r.hmset(key, {'tokens': tokens, 'last_refill': last_refill})
r.expire(key, 60)
return False
# Uso
if not is_allowed('user:123', capacity=10, refill_rate=1):
return Response(status=429, body=b"Rate limit exceeded")
Sliding Window Log (Node.js / En memoria)
const requests = new Map(); // userId -> [timestamps]
function isAllowed(key, limit, windowMs) {
const now = Date.now();
const windowStart = now - windowMs;
if (!requests.has(key)) {
requests.set(key, [now]);
return true;
}
const timestamps = requests.get(key).filter(t => t > windowStart);
timestamps.push(now);
requests.set(key, timestamps);
return timestamps.length <= limit;
}
// Middleware Express
function rateLimitMiddleware(req, res, next) {
const key = req.ip;
if (!isAllowed(key, 100, 60000)) {
return res.status(429).json({ error: 'Too many requests' });
}
next();
}
Fixed Window (Nginx)
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
}
Explicación
- Token bucket: un bucket contiene un número fijo de tokens. Cada petición consume un token. Los tokens se recargan a tasa constante. Esto permite ráfagas controladas mientras se hace cumplir una tasa promedio a lo largo del tiempo. Ideal para APIs que toleran picos cortos.
- Sliding window log: En cada petición, poda entradas fuera de la ventana actual y cuenta el restante. El más preciso pero intensivo en memoria a gran escala.
- Fixed window: divide el tiempo en ventanas discretas (por ejemplo, buckets de 1 minuto). Un contador se incrementa por ventana. Simple y eficiente en memoria, pero una ráfaga en el límite de la ventana cuenta dos veces.
- Limitación distribuida: los contadores en memoria son rápidos pero fallan entre múltiples instancias de servidor. Redis proporciona estado compartido con operaciones atómicas (
INCR,EXPIRE, scripts Lua) para rate limiting distribuido.
Variantes
| Algoritmo | Tolerancia a ráfagas | Uso de memoria | Distribuido | Mejor para |
|---|---|---|---|---|
| Token bucket | Sí | Bajo | Redis | APIs con tolerancia a ráfagas |
| Sliding window | No | Alto | Redis | Límites estrictos por segundo |
| Fixed window | No | Muy bajo | Redis | Techos de tasa simples |
| Leaky bucket | Sí (suavizado) | Bajo | Difícil | Shaping de tráfico |
Lo que funciona
- Retorna 429 con
Retry-After: Esto ayuda a clientes bien comportados a retroceder automáticamente. - Usa diferentes límites por endpoint: los endpoints de autenticación deberían ser más estrictos (5 intentos/minuto) que los endpoints de datos de solo lectura (100 peticiones/minuto). Adapta los límites al costo y sensibilidad de cada operación.
- Identifica clientes correctamente: limita por ID de usuario autenticado cuando esté disponible, no solo por dirección IP. Los NATs compartidos y VPNs pueden causar falsos positivos al limitar solo por IP.
- Implementa límites por tier: los usuarios gratuitos obtienen 100 peticiones/hora, los pagados 10,000.
- Monitorea peticiones rechazadas: un pico repentino en respuestas 429 puede indicar un ataque o un bug de cliente.
Errores comunes
- No manejar skew de reloj: los sistemas distribuidos con desviación de reloj pueden calcular mal los límites de ventana.
- Rate limiting solo en el edge: los gateways edge capturan la mayoría de abusos, pero un servicio interno comprometido puede aún saturar bases de datos downstream. Aplica límites en múltiples capas.
- Bloquear usuarios legítimos después de una ráfaga: un usuario que legítimamente dispara una ráfaga (por ejemplo, paginando resultados) no debería ser bloqueado permanentemente.
- Olvidar limpiar claves Redis: en implementaciones de sliding window, los timestamps antiguos se acumulan indefinidamente. Establece TTLs en claves Redis para auto-expirar datos obsoletos.
Soluciones Avanzadas
Token bucket atómico con Redis Lua
El ejemplo de token bucket en Python de arriba tiene una race condition: entre la lectura y escritura de tokens, otra petición puede modificar la misma clave. Usa un script Lua para atomicidad:
-- token_bucket.lua
-- KEYS[1] = rate limit key
-- ARGV[1] = capacity
-- ARGV[2] = refill_rate (tokens per second)
-- ARGV[3] = current timestamp (unix)
-- ARGV[4] = TTL in seconds
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local ttl = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
local elapsed = now - last_refill
tokens = math.min(capacity, tokens + elapsed * refill_rate)
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, ttl)
return allowed
import time
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# Cargar el script Lua una vez al inicio
TOKEN_BUCKET_SCRIPT = r.register_script(open('token_bucket.lua').read())
def is_allowed_atomic(key: str, capacity: int, refill_rate: float) -> bool:
now = time.time()
result = TOKEN_BUCKET_SCRIPT(
keys=[key],
args=[capacity, refill_rate, now, 60]
)
return bool(result)
# Uso — thread-safe a través de múltiples workers
if not is_allowed_atomic('user:123', capacity=100, refill_rate=10):
return Response(status=429, headers={'Retry-After': '1'},
body=b"Rate limit exceeded")
Express con rate-limiter-flexible (Node.js)
El paquete rate-limiter-flexible soporta limitación distribuida respaldada por Redis con manejo de bursts integrado:
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redis = require('redis');
const redisClient = redis.createClient({
url: process.env.REDIS_URL,
});
// API general: 100 peticiones por 10 segundos
const apiLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'api',
points: 100,
duration: 10,
blockDuration: 60, // Bloquear por 60s después de exceder
});
// Endpoints de auth: más estricto, 5 intentos por 60 segundos
const authLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'auth',
points: 5,
duration: 60,
blockDuration: 300, // Bloquear por 5 minutos después de exceder
});
// Factory de middleware
function createLimiter(limiter, keyFn = (req) => req.ip) {
return async (req, res, next) => {
try {
await limiter.consume(keyFn(req), 1);
next();
} catch (rejRes) {
const retryAfter = Math.ceil(rejRes.msBeforeNext / 1000);
res.set('Retry-After', String(retryAfter));
res.set('X-RateLimit-Limit', String(limiter.points));
res.set('X-RateLimit-Remaining', String(rejRes.remainingPoints || 0));
res.set('X-RateLimit-Reset', new Date(Date.now() + rejRes.msBeforeNext).toISOString());
res.status(429).json({
error: 'Too many requests',
retryAfter,
});
}
};
}
// Aplicar diferentes limiters a diferentes rutas
app.use('/api/', createLimiter(apiLimiter));
app.use('/api/auth/', createLimiter(authLimiter, (req) => req.ip + ':' + (req.body?.email || '')));
Rate limiting en Spring Boot (Java)
import io.github.bucket4j.*;
import io.github.bucket4j.redis.*;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.*;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String clientId = request.getHeader("X-User-ID");
if (clientId == null) {
clientId = request.getRemoteAddr();
}
Bucket bucket = buckets.computeIfAbsent(clientId, this::createBucket);
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
response.setHeader("X-RateLimit-Remaining",
String.valueOf(probe.getRemainingTokens()));
return true;
}
long retryAfter = probe.getNanosToWaitForRefill() / 1_000_000_000;
response.setHeader("Retry-After", String.valueOf(retryAfter));
response.setStatus(429);
response.getWriter().write("Rate limit exceeded");
return false;
}
private Bucket createBucket(String key) {
// 100 peticiones por minuto con burst de 20
Bandwidth limit = Bandwidth.builder()
.capacity(100)
.refillIntervally(100, Duration.ofMinutes(1))
.build();
return Bucket.builder()
.addLimit(limit)
.build();
}
}
Rate limiting por tiers en Nginx
# Definir zones para diferentes tiers
limit_req_zone $binary_remote_addr zone=free:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=paid:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=auth:10m rate=1r/s;
server {
# Endpoints de auth — más estricto
location /api/auth/ {
limit_req zone=auth burst=3 nodelay;
limit_req_status 429;
limit_req_log_level warn;
proxy_pass http://backend;
}
# Tier gratuito — identificado por API key header
location /api/free/ {
limit_req zone=free burst=20 nodelay;
limit_req_status 429;
add_header X-RateLimit-Tier "free" always;
proxy_pass http://backend;
}
# Tier pagado — límites más altos
location /api/paid/ {
limit_req zone=paid burst=50 nodelay;
limit_req_status 429;
add_header X-RateLimit-Tier "paid" always;
proxy_pass http://backend;
}
# Retornar Retry-After en 429
error_page 429 = @ratelimited;
location @ratelimited {
add_header Retry-After "60" always;
return 429 '{"error": "Too many requests", "retryAfter": 60}';
}
} Preguntas frecuentes
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Asegurar APIs con HTTP Security Headers
Cómo configurar headers de seguridad esenciales como HSTS, CSP y X-Frame-Options para proteger APIs y aplicaciones web de ataques comunes.
RecipeProteger Formularios Web Contra Ataques CSRF
Cómo prevenir ataques de Cross-Site Request Forgery usando tokens de sincronización, cookies SameSite y patrones de double-submit cookie.
RecipeTestear Contratos de API con Consumer-Driven Contracts
Cómo prevenir cambios breaking entre microservicios usando contract testing consumer-driven con Pact y validadores de OpenAPI.
GuideChecklist de Seguridad de APIs
Una checklist de seguridad essential para APIs: autenticación, autorización, validación de entrada, rate limiting, encriptación, logging y endurecimiento de despliegue.
GuideAPI Security
Asegurar tus APIs end-to-end. Cubre rate limiting, autenticacion, input validation, CORS, prevencion de SQL injection, API gateway patterns, limites de request size, pagination security, mass assignment, versioning, audit logging y API security testing con ejemplos de codigo.