Rate Limiting de APIs — Diseña Throttling Justo y Útil
Guía práctica para rate limiting de APIs: algoritmos de token bucket, leaky bucket, ventana deslizante, elección de límites e implementación de throttling resiliente para APIs.
Resumen
El rate limiting controla la cantidad de requests de API que un cliente puede hacer en un período de tiempo determinado. Protege tu backend de sobrecarga, asegura compartición justa de recursos y previene abuso. Límites bien diseñados equilibran experiencia de usuario con protección del sistema.
A continuación: algoritmos de rate limiting, estrategias de implementación y elección de límites apropiados.
Cuándo Usar
- Tu API es pública y podría ser abusada por actores maliciosos
- Tienes capacidad de backend limitada y necesitas prevenir sobrecarga
- Ofreces niveles de servicio escalonados (gratis, pro, enterprise)
- Quieres prevenir fallas en cascada durante picos de tráfico
- Necesitas cumplir con acuerdos de consumo de APIs de socios
- Estás formalizando la política — la plantilla de política de rate limiting convierte estas decisiones en un documento que tu equipo firma
Salta el rate limiting cuando la API tiene un único consumidor interno: usa timeouts, backpressure y colas acotadas en su lugar — los límites que controlas en código ganan a requests rechazadas. Y si el problema real es el costo por llamada en vez del volumen de requests, lo que necesitas es una cuota de uso, no un límite por segundo.
Conceptos Clave
| Concepto | Descripción |
|---|---|
| Límite de Tasa | Requests máximas permitidas por ventana de tiempo |
| Cuota | Asignación total sobre un período más largo (ej. llamadas API mensuales) |
| Throttling | Retrasar o rechazar requests que exceden límites |
| Ráfaga | Permiso temporal por encima de la tasa estable |
| Ventana | Período de tiempo sobre el cual se aplican límites |
| Identidad de Cliente | Cómo se identifican llamadores (IP, API key, user ID, org ID) |
Algoritmos de Rate Limiting
Token Bucket
Permite ráfagas hasta la capacidad del bucket manteniendo tasa promedio:
import time
from threading import Lock
class TokenBucket:
def __init__(self, capacity: int, refill_rate: float):
self.capacity = capacity # Tamaño máximo de ráfaga
self.tokens = capacity # Tokens disponibles actualmente
self.refill_rate = refill_rate # Tokens añadidos por segundo
self.last_refill = time.time()
self.lock = Lock()
def allow_request(self, tokens: int = 1) -> 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 >= tokens:
self.tokens -= tokens
return True
return False
# Ejemplo: 10 requests/segundo con ráfaga de 20
bucket = TokenBucket(capacity=20, refill_rate=10)
Mejor para: APIs que necesitan tolerancia a ráfagas (ej. APIs orientadas a usuarios con tráfico esporádico).
Leaky Bucket
Suaviza ráfagas en una tasa de flujo estable:
import time
from collections import deque
from threading import Lock
class LeakyBucket:
def __init__(self, capacity: int, leak_rate: float):
self.capacity = capacity # Tamaño máximo de cola
self.leak_rate = leak_rate # Requests procesados por segundo
self.queue = deque()
self.last_leak = time.time()
self.lock = Lock()
def allow_request(self) -> bool:
with self.lock:
now = time.time()
elapsed = now - self.last_leak
# Remover requests procesados de la cola
to_leak = int(elapsed * self.leak_rate)
for _ in range(min(to_leak, len(self.queue))):
self.queue.popleft()
self.last_leak = now
if len(self.queue) < self.capacity:
self.queue.append(now)
return True
return False
Mejor para: Webhooks, pipelines de procesamiento y situaciones que requieren limitación de tasa estricta.
Sliding Window Log
El más preciso pero intensivo en memoria:
import time
from collections import deque
from threading import Lock
class SlidingWindowLog:
def __init__(self, window_size: int, max_requests: int):
self.window_size = window_size # Segundos
self.max_requests = max_requests
self.requests = deque()
self.lock = Lock()
def allow_request(self) -> bool:
with self.lock:
now = time.time()
cutoff = now - self.window_size
# Remover requests fuera de la ventana
while self.requests and self.requests[0] < cutoff:
self.requests.popleft()
if len(self.requests) < self.max_requests:
self.requests.append(now)
return True
return False
Mejor para: Requerimientos de cumplimiento estricto donde la aplicación exacta importa.
Sliding Window Counter
Aproximación con mejor eficiencia de memoria:
import math
import time
from threading import Lock
class SlidingWindowCounter:
def __init__(self, window_size: int, max_requests: int):
self.window_size = window_size
self.max_requests = max_requests
self.current_window = int(time.time() // window_size)
self.current_count = 0
self.previous_count = 0
self.lock = Lock()
def allow_request(self) -> bool:
with self.lock:
now = int(time.time())
window = now // self.window_size
if window != self.current_window:
self.previous_count = self.current_count
self.current_count = 0
self.current_window = window
# Estimar requests en ventana deslizante
elapsed = now % self.window_size
weight = 1 - (elapsed / self.window_size)
estimated = (self.previous_count * weight) + self.current_count
if estimated < self.max_requests:
self.current_count += 1
return True
return False
Mejor para: APIs de alto tráfico donde la eficiencia de memoria es importante.
Elegir los Límites
Factores a Considerar
| Factor | Guía |
|---|---|
| Costo de endpoint | Endpoints caros (ML, reportes) obtienen límites más bajos |
| Nivel de usuario | Gratis: 100/hr, Pro: 10,000/hr, Enterprise: custom |
| Restricciones de recursos | Limitar basado en capacidad backend, no números arbitrarios |
| Justicia | Límites por usuario previenen que un cliente sature a otros |
| Valor de negocio | Proteger endpoints generadores de ingresos más estrictamente |
Ejemplo de Límites por Nivel
# Ejemplo: Límites de tasa escalonados para API SaaS
tiers:
free:
requests_per_minute: 60
requests_per_hour: 1000
requests_per_day: 10000
burst: 10
pro:
requests_per_minute: 600
requests_per_hour: 10000
requests_per_day: 100000
burst: 100
enterprise:
requests_per_minute: 6000
requests_per_hour: 100000
requests_per_day: 1000000
burst: 1000
Estrategias de Implementación
Rate Limiting a Nivel de Gateway
Aplicar límites en el API gateway para control centralizado:
# Ejemplo: Rate limiting de NGINX
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
map $http_x_api_key $api_key { default $binary_remote_addr; }
limit_req_zone $api_key zone=pro:10m rate=100r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
}
Rate Limiting a Nivel de Aplicación
Control fino dentro de tu aplicación:
from fastapi import FastAPI, HTTPException, Request
from fastapi_limiter import FastAPILimiter
import redis.asyncio as redis
from fastapi import Depends
from fastapi_limiter.depends import RateLimiter
app = FastAPI()
@app.on_event("startup")
async def startup():
app.state.redis = await redis.from_url("redis://localhost")
await FastAPILimiter.init(app.state.redis)
@app.get("/api/data", dependencies=[Depends(RateLimiter(times=100, minutes=1))])
async def get_data(request: Request):
# fastapi-limiter verifica la request antes del handler;
# la clave por defecto es la IP del cliente — pasa un callback
# de identificador si limitas por API key
return {"data": "..."}
Rate Limiting Distribuido
Compartir estado entre múltiples instancias:
# Token bucket distribuido basado en Redis
import redis
class RedisTokenBucket:
def __init__(self, redis_client: redis.Redis, key: str, capacity: int, refill_rate: float):
self.redis = redis_client
self.key = key
self.capacity = capacity
self.refill_rate = refill_rate
def allow_request(self, tokens: int = 1) -> bool:
lua_script = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local tokens_requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local current_tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
local elapsed = now - last_refill
local new_tokens = math.min(capacity, current_tokens + elapsed * refill_rate)
if new_tokens >= tokens_requested then
new_tokens = new_tokens - tokens_requested
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 1
else
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 0
end
"""
return self.redis.eval(
lua_script, 1, self.key,
self.capacity, self.refill_rate, tokens, time.time()
) == 1
Headers de Respuesta HTTP
Comunicar límites claramente a clientes:
| Header | Descripción | Ejemplo |
|---|---|---|
X-RateLimit-Limit | Requests máximas permitidas | 100 |
X-RateLimit-Remaining | Requests restantes en ventana | 42 |
X-RateLimit-Reset | Timestamp Unix cuando el límite resetea | 1704067200 |
Retry-After | Segundos para esperar antes de reintentar | 60 |
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704067200
Retry-After: 60
{
"error": "Límite de tasa excedido",
"message": "Has excedido 100 requests por minuto. Reintenta después de 60 segundos.",
"retry_after": 60
}
Comparando los Algoritmos
| Algoritmo | ¿Exacto? | Memoria | Ráfagas | Mejor uso |
|---|---|---|---|---|
| Ventana fija | No — ráfagas en bordes | O(1) | Sí, sin control | APIs internas, protección aproximada |
| Token bucket | Aproximado | O(1) | Sí, controladas | APIs públicas — el default común |
| Leaky bucket | Flujo exacto | O(n) cola | No — suaviza a tasa estable | Webhooks, pipelines de procesamiento |
| Sliding log | Exacto | O(n) por cliente | No | Cumplimiento estricto |
| Sliding counter | ~99% preciso | O(1) | No | APIs de alto tráfico |
El verdadero trade-off es precisión contra memoria. Un sliding log guarda un timestamp por request por cliente — manejable con 1.000 clientes a 100 requests, brutal con un millón. El sliding window counter se acerca a unos pocos puntos de la precisión del log con memoria O(1) ponderando el conteo de la ventana anterior. Token y leaky bucket responden preguntas distintas: token bucket pregunta “¿puede entrar esta request?” mientras leaky bucket pregunta “¿cuándo puede salir?” Elige el segundo solo cuando un flujo estable importa más que la tolerancia a ráfagas.
Cuando el Store de Rate Limit Falla
Los limitadores distribuidos tienen un modo de fallo que nadie ve hasta que ocurre: Redis se cae y cada allow_request o lanza error o devuelve silenciosamente false. Decide el comportamiento explícitamente:
- Fail-open: permite requests cuando el store no responde. Mantiene la API viva durante una caída — y deja pasar un ataque real justo en esa ventana.
- Fail-closed: rechaza (o devuelve un 429 seguro) cuando el store no responde. Protege el backend; convierte un parpadeo de Redis en una caída visible para el cliente.
- Fallback local: mantén un pequeño token bucket en proceso como última línea. Los límites se degradan a valores por instancia — más laxos, pero acotados y predecibles.
La mayoría de los equipos elige fail-open con fallback local y alerta agresivamente sobre anomalías de 429. Elijas lo que elijas, ponlo en el documento de política — “qué hace el limitador cuando su store está caído” es una decisión de producto, no un detalle de implementación.
Ajustando el Comportamiento del Cliente
Un rate limit es un contrato, y los headers son la parte legible. Tres reglas que mantienen a los clientes honestos: fija Retry-After desde el reset real de la ventana, no a ojo; envía X-RateLimit-Remaining también en respuestas exitosas, no solo en 429s, para que los clientes bien portados se auto-limiten antes de chocar contra el muro; y documenta la semántica exacta del reset (borde fijo frente a ventana móvil). Para los clientes, la reacción correcta a un 429 es backoff exponencial con jitter — reintentar exactamente en Retry-After al unísono entre miles de clientes recrea el pico que acabas de rechazar.
Probando y Monitoreando los Límites
Los límites que nunca se prueban suelen estar rotos. Tres chequeos baratos pertenecen al CI: dispara requests pasando el límite y verifica que el primer 429 llega exactamente en límite + 1; comprueba que la respuesta lleva Retry-After y el cuerpo JSON que tus docs prometen; y corre todo con el reloj del store desfasado — los bugs de borde de ventana solo aparecen cuando dos requests a caballo cruzan una frontera. En producción, alerta sobre la tasa de 429, no solo el conteo absoluto: un salto repentino significa o un error de integración o alguien sondeando. Una subida lenta y constante significa que tus límites están demasiado ajustados para el uso real — o que un cliente mal portado necesita una conversación, no una cuota mayor.
Calculando un Límite desde la Capacidad
Trabaja hacia atrás desde lo que el backend puede servir, no hacia adelante desde lo que suena razonable. Si el servicio sostiene 2.000 RPS y POST /orders cuesta tres veces una lectura barata, el presupuesto de escritura es aproximadamente 2.000 / (1 + 2·fracción_de_escritura). Empieza conservador — un límite demasiado estricto durante dos semanas tiene arreglo; un límite demasiado laxo durante un incidente real, no. Después instrumenta: publica los números en la documentación de la API, porque un límite que nadie conoce se confunde con un bug.
El Rate Limiting en el Contexto Mayor
El rate limiting es una capa, no toda la defensa. Las cuotas acotan el consumo total por período de facturación — un cliente dentro de su límite por segundo puede quemar un millón de llamadas al mes. Los límites de concurrencia tapan las requests en vuelo y protegen los thread pools cuando las requests se vuelven lentas; un límite de tasa solo no hace nada cuando cada request tarda 30 segundos porque todas “caben” en la ventana. El throttling por costo trata un reporte caro como una docena de lecturas baratas en vez de una request. Y una capa WAF o de bots delante maneja lo que el rate limit no puede: tráfico que directamente no quieres, no solo demasiado.
Con el orden de capas correcto, cada pieza se mantiene simple. El stack típico: el WAF descarta tráfico claramente hostil, el gateway aplica límites gruesos por key, los limitadores por servicio protegen rutas calientes por dentro, y las cuotas manejan la dimensión de negocio. La identidad importa tanto como los números — un límite por API key falla cuando un cliente emite diez keys, así que mapea los límites a la identidad que realmente paga o genera carga (org ID, no la key).
Comunicando los Límites a los Clientes
Publicar los números es la mitad del trabajo; publicar la semántica es el resto. Los clientes necesitan saber el modelo de ventana (fija o móvil), si los límites son por key, por org o por endpoint, cuál es la tolerancia de ráfaga y cómo se ve el cuerpo del 429. Una página de docs que solo dice “100 req/min” genera un ticket de soporte la primera vez que un cliente choca contra la cuota horaria invisible.
Haz los límites visibles donde menos duelen: en el dashboard del desarrollador, en el flujo de onboarding y dentro del SDK si distribuyes uno — un cliente que maneja Retry-After automáticamente convierte un 429 de error en un badén de velocidad. Cuando cambies límites, trátalo como un breaking change: aviso previo, una ventana de deprecación con headers anunciando el nuevo techo y un canal explícito de anuncios. Sorprender a un cliente que paga con un límite más estricto cuesta más confianza que la sobrecarga que pretendías prevenir.
Un patrón que se paga solo rápido es una página de “límites” que muestra los números de cada nivel junto al uso actual del cliente — los mismos números que llevan los headers, dibujados como barras de progreso. Cuando un cliente puede verse acercarse al techo, el 429 deja de ser una sorpresa y pasa a ser una señal sobre la que actuó antes. Combínalo con una vía de upgrade self-service y el límite se convierte en embudo de crecimiento.
Por último, ensaya el fallo antes de que producción lo haga por ti: apunta un generador de carga a un gateway de staging con la mitad de tus límites previstos y observa la tasa de 429, los reintentos del cliente y el CPU del backend — diez minutos de datos resuelven la mayoría de las discusiones sobre qué números publicar.
Lo que funciona
- Retorna mensajes de error informativos. Dile a clientes exactamente qué límite golpearon y cuándo pueden reintentar.
- Usa diferentes límites por endpoint. Endpoints de búsqueda pueden tolerar límites más altos que endpoints de escritura.
- Implementa backoff exponencial en clientes. Respuestas 429 deberían activar backoff, no reintentos inmediatos.
- Monitorea saturaciones del límite. Picos repentinos en 429s pueden indicar ataques o problemas de integración.
- Permite períodos de gracia para clientes nuevos. Empieza con límites generosos y ajusta basado en patrones de uso.
- Documenta límites claramente. Publica límites de tasa en tu documentación de API.
Errores Comunes
- Usar direcciones IP como único identificador. NAT y redes móviles comparten IPs; usa API keys o user IDs.
- Problemas de vecino ruidoso. Un usuario pesado no debería impactar a otros; aplica límites por cliente.
- Ignorar tráfico de ráfaga. Usuarios legítimos pueden hacer ráfagas durante carga de página; permite ráfagas cortas.
- Límites inconsistentes entre servicios. Estandariza límites por nivel y tipo de endpoint.
- Olvidar manejar casos límite. ¿Qué pasa cuando el almacén de límite de tasa (Redis) está caído?
Variantes
- Limitación de concurrencia: Limitar requests en vuelo simultáneos en lugar de tasa por tiempo.
- Rate limiting adaptativo: Ajustar límites dinámicamente basado en salud de backend (límites más bajos cuando está sobrecargado).
- Rate limiting geográfico: Aplicar diferentes límites basados en ubicación del cliente o requerimientos regulatorios.
- Throttling basado en costo: Limitar operaciones caras (inferencia ML, generación de reportes) más estrictamente.
Lectura Adicional
- RFC 9110 — HTTP Semantics, 429 Too Many Requests — el código de estado y la semántica de
Retry-After - IETF draft — RateLimit Fields for HTTP — la familia de headers en estandarización
- Redis — Rate limiter pattern — implementación Lua de ventana fija sobre Redis
- NGINX — ngx_http_limit_req_module — el módulo detrás del ejemplo de gateway
Preguntas frecuentes
¿Cuál es un buen límite de tasa por defecto para una API pública?
100 requests por minuto por usuario es un punto de partida razonable — luego ajusta según el uso real y lo que tu backend puede servir. Vigila los 429 tras el lanzamiento: si usuarios legítimos chocan seguido contra el techo, está muy ajustado.
¿Cómo manejo el rate limiting en una arquitectura de microservicios?
Aplica en el API gateway para tráfico externo — una política, un solo lugar. Dentro de la malla, usa límites por servicio (Istio, Linkerd, o buckets a nivel app) para que un llamante interno hablador no ahogue sus dependencias.
¿Debería limitar tráfico autenticado y no autenticado de forma distinta?
Sí. Los usuarios autenticados reciben límites por usuario más altos, porque sabes quién llama. El tráfico no autenticado recibe límites por IP más estrictos — sin identidad, la dirección es lo único que tienes.
¿Cómo prevengo abuso sin afectar a usuarios legítimos?
Escala en vez de banear de entrada: avisos, luego bloqueos temporales, luego bans — con vía de apelación para falsos positivos. Los usuarios legítimos saturan el límite en ráfagas; los abusadores lo saturan constantemente — esa es la señal que buscas.
¿Token bucket o ventana fija — qué algoritmo elijo?
Token bucket tolera ráfagas legítimas y suaviza el tráfico — el default común para APIs públicas. La ventana fija es más simple pero permite ráfagas en los bordes de ventana. El sliding log es el más preciso pero cuesta memoria por entrada; el sliding window counter logra casi la misma precisión a una fracción del coste.
Recursos Relacionados
Guía de Diseño de APIs REST
Una Referencia Detallada para diseñar APIs REST limpias, escalables y mantenibles.
GuideDiseño de API Gateway: Resiliencia, Enrutamiento y Seguridad
Guía práctica para diseñar API gateways: enrutamiento, autenticación, rate limiting, circuit breakers, caché y patrones de despliegue para APIs resilientes.
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.
GuideGuía de Optimización de Performance Web
manual detallado para optimizar el rendimiento de aplicaciones web con mejores Core Web Vitals y experiencia de usuario.
GuideIngenieria de confiabilidad del sitio (SRE)
Guia practica de SRE: definir SLIs, SLOs y SLAs, gestionar presupuestos de error, reducir toil, rotaciones de guardia y construir una cultura de confiabilidad.