Rate Limiting de APIs
Protege las APIs de abuso y asegura uso justo de recursos con token bucket, sliding window y leaky bucket.
Visión General
Un scraper empieza a golpear tu API pública a 2.000 peticiones por segundo. Tu pool de conexiones a la base de datos se satura, los usuarios legítimos empiezan a ver timeouts, y la factura del grupo de autoscaling se triplica de la noche a la mañana. El rate limiting es la línea entre ese incidente y un encogimiento de hombros: un middleware que responde “¿cuántas?” antes de que tus handlers respondan cualquier otra cosa.
Tres algoritmos cubren la mayoría de las necesidades en producción. Token bucket rellena un pool de tokens a una tasa fija. Cada petición gasta un token, así que los bursts cortos pasan mientras la tasa media se mantiene acotada. Sliding window cuenta las peticiones dentro de una ventana temporal deslizante y rechaza todo lo que pase del límite. Estricto, sin picos en los bordes. Leaky bucket drena las peticiones encoladas a una tasa constante, suavizando una entrada irregular en una salida predecible.
Esta receta te da implementaciones funcionales en Python, JavaScript y Java, los trade-offs entre ellas, y los detalles de producción que deciden si tu limiter te protege o se convierte en el outage: política de fallo de Redis, límites por tenant, respuestas 429 correctas, y qué monitorear. Para un recorrido de diseño más profundo, consulta la Guía de Rate Limiting de APIs; para un deep dive solo de Redis, mira Rate Limiting de API con Redis.
Cuándo Usar
Usa rate limiting cuando:
- APIs públicas necesitan protección contra brute force, scraping y credential stuffing
- Diferentes tiers de usuarios (anónimos, autenticados, premium) requieren cuotas distintas
- Múltiples nodos de API deben compartir el estado del rate limit de forma consistente
- Endpoints costosos (exports, reports) necesitan límites más ajustados que las lecturas baratas
Omítelo para tráfico interno service-to-service de confianza y receptores de webhooks que ya verifican firmas — ver Cuándo No Usar más abajo.
En la práctica, la mayoría de APIs públicas terminan con un modelo por tiers: las peticiones anónimas reciben un techo ajustado por IP, los usuarios autenticados un techo por cuenta según su plan, y los servicios internos un límite eximido o mucho más alto. Diseña primero la key y la tabla de tiers; la elección del algoritmo viene después.
Solución
Python (Token Bucket con Redis)
import time
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def token_bucket(key, capacity=10, refill_rate=1.0):
"""Token bucket usando un hash de Redis con expiración.
NOTA: hgetall + hset no es atómico. Dos peticiones concurrentes
pueden leer el mismo contador y ambas pasar. Para enforcement
estricto usa un script Lua (ver recursos companion) — la versión
no atómica se desvía ~1 petición bajo concurrencia, suficiente
para la mayoría de APIs.
"""
now = time.time()
bucket_key = f"rate_limit:{key}"
tokens = r.hgetall(bucket_key)
if not tokens:
r.hset(bucket_key, mapping={"tokens": capacity - 1, "last_refill": now})
r.expire(bucket_key, int(capacity / refill_rate) + 1)
return True
current_tokens = float(tokens.get(b"tokens", 0))
last_refill = float(tokens.get(b"last_refill", now))
elapsed = now - last_refill
current_tokens = min(capacity, current_tokens + elapsed * refill_rate)
if current_tokens < 1:
r.hset(bucket_key, mapping={"tokens": current_tokens, "last_refill": now})
return False
r.hset(bucket_key, mapping={"tokens": current_tokens - 1, "last_refill": now})
return True
JavaScript (Sliding Window con Redis)
import { createClient } from 'redis';
const client = createClient({ url: 'redis://localhost:6379' });
await client.connect();
async function slidingWindow(key, limit = 100, windowSec = 60) {
const now = Date.now();
const windowStart = now - windowSec * 1000;
const sortedSetKey = `rate_limit:${key}`;
// Eliminar entradas fuera de la ventana
await client.zRemRangeByScore(sortedSetKey, 0, windowStart);
// Contar entradas actuales
const count = await client.zCard(sortedSetKey);
if (count >= limit) {
return { allowed: false, retryAfter: windowSec };
}
// Agregar petición actual
await client.zAdd(sortedSetKey, [{ score: now, value: `${now}` }]);
await client.expire(sortedSetKey, windowSec);
return { allowed: true, remaining: limit - count - 1 };
}
El sorted set guarda un miembro por petición con el timestamp como score. Usa más memoria que un contador: un limiter de 100 req/s ocupa ~128 bytes por key — pero te da una ventana deslizante real en lugar del pico en el borde de la ventana fija. Móntalo como middleware antes de tus route handlers para que las peticiones rechazadas nunca toquen tu lógica de negocio, y devuelve retryAfter tanto en el cuerpo JSON como en el header Retry-After.
Java (Leaky Bucket)
import java.util.concurrent.ConcurrentHashMap;
public class LeakyBucket {
private static final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();
static class Bucket {
final int capacity;
final double leakRatePerSec;
double water;
long lastLeak;
Bucket(int capacity, double leakRatePerSec) {
this.capacity = capacity;
this.leakRatePerSec = leakRatePerSec;
this.water = 0;
this.lastLeak = System.currentTimeMillis();
}
synchronized boolean allow() {
long now = System.currentTimeMillis();
double elapsed = (now - lastLeak) / 1000.0;
water = Math.max(0, water - elapsed * leakRatePerSec);
lastLeak = now;
if (water < capacity) {
water += 1;
return true;
}
return false;
}
}
public static boolean allowRequest(String clientId, int capacity, double leakRate) {
return buckets.computeIfAbsent(clientId, k -> new Bucket(capacity, leakRate)).allow();
}
}
El synchronized en allow() mantiene cada bucket consistente bajo hilos concurrentes; ConcurrentHashMap maneja la búsqueda por cliente. Esta versión vive en memoria; bien para un despliegue de un solo nodo o un sidecar de gateway. Para consistencia multi-nodo moverías las mismas matemáticas de drenado a Redis (o simplemente usarías el sliding window de arriba).
Explicación
La ruta de la petición nunca cambia. El limiter simplemente se sienta delante:
Token bucket mantiene un pool de tokens que se rellena a una tasa fija. Cada petición consume uno; un pool vacío significa rechazo. Los bursts hasta la capacidad pasan, mientras la media a largo plazo se queda en la tasa de relleno. Elígelo para APIs públicas generales donde los clientes ocasionalmente tienen picos.
Sliding window guarda los timestamps de las peticiones de la ventana deslizante actual y rechaza cuando el conteo llega al límite. A diferencia de las ventanas fijas (que dejan pasar 2× el límite en el borde), la ventana se desliza con cada petición. Elígelo para pagos o endpoints sensibles donde el límite debe ser exacto.
Leaky bucket encola las peticiones entrantes y las drena a una tasa constante, como un cubo con un agujero. Los bursts de llegada no importan; la salida siempre es suave. Elígelo cuando proteges un servicio downstream que no puede absorber bursts en absoluto.
Un ejemplo concreto deja clara la diferencia. Un cliente dispara 20 peticiones en un segundo contra un límite de 10/segundo. Con un token bucket de capacidad 10, las primeras 10 pasan al instante y las siguientes 10 se rechazan. El burst era legal, la tasa sostenida no. Con una sliding window al mismo límite, las primeras 10 pasan y el resto espera a que la ventana avance. Con un leaky bucket, las 20 peticiones se encolan y drenan a una por 100ms; ninguna se rechaza, pero la última termina 2 segundos después. Mismo límite, tres experiencias de usuario distintas.
La otra decisión es la key. rate_limit:ip:203.0.113.7 limita una dirección; rate_limit:user:u_4812 limita una cuenta; rate_limit:key:ak_live_9f3 limita una credencial de API. La key correcta suele ser la identidad más específica en la que puedas confiar: una API key para APIs de pago, user ID para tráfico autenticado, IP solo como fallback para endpoints anónimos.
Variantes
| Algoritmo | Manejo de Bursts | Memoria | Ideal Para |
|---|---|---|---|
| Token bucket | Permite bursts hasta la capacidad | Baja | APIs con tráfico irregular |
| Sliding window | Límite estricto por ventana | Media | APIs de pagos, endpoints sensibles |
| Leaky bucket | Suaviza a tasa constante | Baja | Protección de servicios downstream |
Para el algoritmo token bucket solo en Redis, mira Rate Limiting con Token Bucket en Redis; para una implementación específica de FastAPI, Rate Limiting de API en Python cubre los hooks del framework.
Casos borde que conviene conocer antes de elegir. Fixed window (no mostrada) es la implementación más barata, un INCR más EXPIRE, pero permite que un cliente dispare la cuota completa al final de una ventana y otra vez al inicio de la siguiente, duplicando efectivamente el límite en el borde. Sliding log es la más estricta y la más cara, guardando cada timestamp. Los tres algoritmos de arriba son el punto medio que los sistemas en producción realmente ejecutan.
Lo que Funciona — y lo que Rompe
Hazlo:
- Usa token bucket para bursts controlados y sliding window para límites estrictos
- Devuelve
Retry-Aftercon las respuestas 429 para que los clientes sepan cuándo retroceder - Limita por user ID o API key, no solo por IP. Un NAT corporativo pone miles de usuarios tras una sola dirección
- Loguea las violaciones de rate limit para monitoreo de seguridad y detección de abuso
- Falla abierto alrededor de Redis: un rate limiter muerto bloqueando todo el tráfico es un outage peor que una API temporalmente sin límite
- Diferencia los límites por costo del endpoint: un GET
/userses barato; un POST/reports/exportno - Guarda los contadores en Redis, no en memoria; los contadores en memoria no sobreviven despliegues multi-instancia
- Exime las llamadas internas service-to-service o dales un techo mucho más alto
No lo hagas:
- Limitar solo por IP y bloquearás usuarios legítimos tras NAT y no pillarás atacantes con rotación
- Devolver 429 sin
Retry-After, así que los clientes no pueden implementar backoff correcto y te golpearán más fuerte - Usar un solo rate limit para todos los endpoints sin importar costo o sensibilidad
- Dejar que el limiter falle cerrado en silencio. Redis caído + fail closed = outage autoinfligido
- Ignorar el log de violaciones. Un pico de 429s es un límite mal configurado o un ataque en curso
- Usar ventanas fijas sin jitter, así que todos los clientes reintentan a la vez en el borde de la ventana (thundering herd)
- Olvidar la capa de proxy. Tras un load balancer,
req.ipes la IP del proxy hasta que configures la confianza enX-Forwarded-For - Aplicar límites a GET pero no a otros métodos, y los atacantes cambian a POST/PUT para evadirlos
Checklist de Producción
- Rate limits aplicados antes del procesamiento de la petición, no después
- Respuestas 429 incluyen
Retry-After,X-RateLimit-Limit,X-RateLimit-Remaining - Contadores con scope por API key o usuario, no solo por IP
- Endpoints costosos con límites separados y más bajos
- Límites aplicados a todos los métodos HTTP, incluidos OPTIONS y HEAD
-
trust proxyconfigurado;X-Forwarded-Forvalidado contra IPs de proxy confiables - Política de fallo de Redis elegida deliberadamente (fail open para la mayoría de APIs) con alertas
- Tráfico interno service-to-service exento o con techo más alto
- Jitter (delay aleatorio de 0-500ms) en la lógica de reintento del cliente para prevenir estampidas en el borde
- Configuración de rate limit externalizada (env vars, config service) para hot-reload
- Límites probados en staging con tráfico concurrente realista
- Tasa de 429 monitoreada; alerta por encima del ~5% del tráfico total
- Límites documentados en la documentación de la API y el portal de desarrolladores
Escalado y Rendimiento
| Estrategia | Overhead por petición | Throughput máximo | Memoria por key |
|---|---|---|---|
| Token bucket en memoria | 0.01ms | 500K req/s | 64 bytes |
| Redis fixed window | 1-2ms | 50K req/s | 72 bytes |
| Redis sliding window | 2-3ms | 30K req/s | 128 bytes |
| Redis token bucket | 1-2ms | 50K req/s | 64 bytes |
| Cloudflare edge | 0ms (en origen) | Ilimitado | 0 bytes |
En memoria es 100× más rápido que Redis pero no funciona entre instancias. Para despliegues de una sola instancia es la opción correcta; para cualquier cosa distribuida, Redis es el estándar. El rate limiting en el edge (Cloudflare, Fastly) absorbe el abuso antes de que llegue a tu origen. Para APIs públicas bajo ataque, suele ser lo primero que se compra.
La matemática a escala: cada check cuesta 1-3ms (un round-trip a Redis), y cada check toma ~2 operaciones. A 1M de peticiones/segundo necesitas Redis Cluster con sharding por API key (~2M ops/s en el pico). Si tu presupuesto de latencia es menor a 10ms, usa token buckets locales con sincronización periódica a Redis cada ~100ms. Cambias ~10% de precisión bajo contención por velocidad. En despliegues multi-región, Redis debe estar co-localizado con las instancias de cada región; las llamadas cross-region añaden 50-200ms y anulan el propósito.
| Componente | Costo | Notas |
|---|---|---|
| Redis (single, 1GB) | $10-$25/mes | AWS ElastiCache, GCP Memorystore |
| Redis Cluster (3 shards) | $75-$200/mes | Para >100K ops/segundo |
| Cloudflare Rate Limiting | $5-$25/mes | A nivel edge, sin Redis |
| AWS API Gateway throttling | $0 | Integrado, límites por stage |
Consideraciones de Seguridad
- Suplantación de keys: los clientes que pueden falsear su IP o user ID evaden el límite. Valida
X-Forwarded-Forcontra las IPs de tus proxies confiables. Confiar ciegamente en él es peor que ignorarlo. - DDoS por debajo del límite: los atacantes envían tráfico justo por debajo del límite desde muchas IPs para agotar recursos sin disparar nunca un 429. Combina rate limits con límites de conexiones y de tamaño de cuerpo; el OWASP API Security Top 10 cubre el consumo de recursos sin restricción por algo.
- Redis como single point of failure: usa modo Sentinel o Cluster. Un limiter que bloquea todo el tráfico cuando el store muere es una pieza crítica de disponibilidad.
- Fuga de información en headers:
X-RateLimit-LimityX-RateLimit-Remainingrevelan tu configuración a los atacantes. Considera omitirlos o aproximarlos para tráfico no autenticado. El emergente estándar de headers RateLimit te da una alternativa más limpia a los headersX-personalizados. - Cambio de método: los límites aplicados solo a GET se evaden trivialmente vía POST, PUT o incluso OPTIONS.
- Variación de path:
/api/users?x=1y/api/users?x=2son keys distintas para un limiter ingenuo. Normaliza las URLs y limita por patrón de endpoint, no por path completo. - Timing attacks: si las peticiones denegadas se resuelven mediblemente más rápido que las permitidas, los atacantes pueden inferir el estado del límite. Devuelve las decisiones tempranas por la misma ruta de código.
Monitoreo y Testing
Vigila cuatro señales. Tasa de respuestas 429: un salto repentino significa un límite mal configurado o un cliente abusivo; alerta por encima del ~5% del tráfico. Latencia de Redis en los checks: si el p95 supera 5ms, tu limiter es ahora el cuello de botella. Crecimiento del conteo de keys: un crecimiento sin límite significa que los clientes generan IDs únicos para evadir límites; pon un techo y expira agresivamente. Fallos del limiter: un limiter caído o un Redis inalcanzable necesita una alerta inmediata, porque acabas de perder tu protección o tu disponibilidad.
Prueba cuatro escenarios. Enforcement: envía N+1 peticiones rápidamente; la (N+1) debe devolver 429 con los headers correctos. Reset de ventana: peticiona en el borde del límite (59.9s, 60.1s) y verifica que el reset se comporta como está documentado. Failover de Redis: mata Redis a mitad del test y confirma que el limiter falla como dice tu política (abierto o cerrado), no al revés. Consistencia distribuida: corre instancias en paralelo y verifica que el límite agregado se mantiene en todas.
Troubleshooting
- Los límites no se aplican entre instancias. Los contadores están en memoria, no en Redis, así que cada nodo tiene su propio límite. Mueve el estado a un store compartido.
- Todos reciben 429 tras un proxy.
req.ipes la IP del load balancer; configuratrust proxyy leeX-Forwarded-For. - Usuarios corporativos legítimos bloqueados. Están tras un NAT compartiendo una IP. Cambia la key a user ID o API key.
- La API se cae cuando Redis se cae. El limiter falla cerrado. Decide tu política deliberadamente: fail open para la mayoría de APIs públicas, fail closed solo para endpoints donde el acceso ilimitado es peor que el downtime.
- Thundering herd en los bordes de ventana. Las ventanas fijas se resetean para todos a la vez y todos los clientes reintentan simultáneamente. Usa sliding windows o añade jitter a los reintentos.
- 429s pero nadie respeta
Retry-After. Algunos clientes ignoran el header por diseño. Loguea a los infractores y considera bloqueos más largos para reincidentes.
Cuándo No Usar Este Enfoque
- Microservicios internos con llamadores de confianza — limitar el tráfico inter-servicio añade latencia y riesgo de fallos en cascada sin ganancia de seguridad. Usa cuotas de servicio en el gateway.
- Receptores de webhooks con verificación de firma — si cada petición pasa un check HMAC, el tráfico inválido ya se rechaza antes de que corra el limiter.
- APIs de procesamiento por lotes — un batch job enviando 10K peticiones en 10 segundos es comportamiento esperado, no abuso. Usa throttling basado en colas en lugar de límites por petición.
Ver también
- Guía de Rate Limiting de APIs — el recorrido de diseño detrás de esta receta
- Rate Limiting de API con Redis — deep dive de implementación específica de Redis
- Guía de Checklist de Seguridad de APIs — el rate limiting como una capa de defensa de APIs
- Plantilla de Respuesta de Error de API — formatea el cuerpo del 429 correctamente
- Comandos de Redis —
INCR,EXPIRE,ZADD,ZREMRANGEBYSCOREusados en estas implementaciones - Draft de headers RateLimit — el estándar emergente que reemplaza los headers
X-RateLimit-*personalizados - OWASP API Security Top 10 — consumo de recursos sin restricción y lo que los límites solos no pueden parar
Código companion: recursos de rate limiting de API — token bucket atómico con Lua, middleware de Express y una suite de pytest para los escenarios de arriba.
Preguntas frecuentes
¿Debería fallar abierto o cerrado cuando Redis se cae?
Falla abierto para la mayoría de APIs públicas. Rechazar todas las peticiones porque el limiter no está disponible es un outage peor que el tráfico temporalmente ilimitado. Loguea el fallo y alerta sobre él. Falla cerrado solo para endpoints donde el acceso ilimitado es más peligroso que el downtime: operaciones de admin, reset de contraseñas.
¿Cómo manejo rate limiting distribuido entre múltiples nodos?
Usa un store compartido como Redis. Cada nodo verifica e incrementa el mismo contador. Para menor latencia, usa token buckets locales con sincronización periódica a Redis cada ~100ms; aceptas ~10% de imprecisión bajo contención a cambio de no añadir un salto de red a cada petición.
¿Con qué valores de rate limit debería empezar?
100 peticiones/minuto para usuarios autenticados, 10/minuto para tráfico anónimo. Vigila tu tasa de 429 durante dos semanas: si los usuarios legítimos chocan con los límites, súbelos; si nadie los toca nunca, el límite es como si no existiera. Los endpoints costosos (exports, reports) llevan sus propios límites más ajustados.
¿Fixed window o sliding window — cuál elijo?
Sliding window para cualquier cosa de cara al usuario. Las ventanas fijas dejan pasar 2× el límite en el borde (un cliente puede disparar la cuota completa a los 59.9s y otra vez a los 60.1s) y causan reintentos en estampida. La ventana fija solo gana cuando necesitas la implementación más barata posible (un solo INCR + EXPIRE).
¿Cómo aplico rate limiting a una API GraphQL?
Los límites por petición pierden el punto: una query puede costar 50× más que otra. Limita por costo o profundidad de query (asigna puntos por campo, tope de puntos por ventana), o limita en el gateway por nombre de operación para las operaciones caras conocidas.
¿Debería el cuerpo del 429 usar problem details de RFC 9457?
Sí — application/problem+json da a los clientes un type, title, status y detail parseables en lugar de un string ad-hoc. Incluye Retry-After como header (es lo que los intermediarios y las librerías HTTP realmente leen) y pon la explicación humana en detail.
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
GuideChecklist de Seguridad API: Autenticación a Encriptación
Checklist de seguridad esencial para APIs: autenticación, autorización, validación de entrada, rate limiting, encriptación, logging y endurecimiento de despliegue.
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 de API en Python con Token Bucket
Implementa token bucket rate limiting en Flask y FastAPI con soporte Redis.