
Una API sin límite de tráfico tiene un único cliente mal configurado a distancia de una caída. Un script en bucle sin sleep, un bug que reintenta sin backoff, o directamente alguien haciendo scraping — todos generan el mismo síntoma: miles de peticiones por segundo consumiendo los mismos recursos que necesita cualquier otro usuario. Rate limiting es la disciplina de poner un techo a ese tráfico antes de que llegue a hacer daño.
Dónde vive el límite
Un límite se define siempre sobre una clave y una ventana de tiempo: N peticiones por clave cada T segundos. La clave puede ser distinta según lo que quieras proteger:
- Por API key o usuario: para repartir el uso de forma justa entre clientes — típico en APIs con planes de pago.
- Por IP: para frenar abuso anónimo, como intentos de login por fuerza bruta, cuando todavía no hay usuario autenticado.
- Por endpoint: un
/buscarque hace una consulta cara a base de datos necesita un límite más estricto que un/ping. - Global: un techo para todo el servicio, como última defensa cuando fallan los límites más finos.
Fixed window: el algoritmo obvio, y su problema

La implementación más directa es un contador que se reinicia cada intervalo fijo: 100 peticiones por minuto significa un contador que sube con cada petición y vuelve a cero en cada minuto exacto (por ejemplo, en :00).
El problema aparece en el borde de la ventana. Si un cliente agota su cupo de 100 peticiones en los últimos segundos del minuto 1, y el contador se resetea en seco al empezar el minuto 2, ese mismo cliente puede lanzar otras 100 peticiones de inmediato. Resultado real: hasta 200 peticiones en una ventana de 10 segundos, sin que ninguna cuenta individual haya superado su límite formal.
Token bucket: la alternativa que se usa en producción

En vez de contar peticiones dentro de ventanas fijas, el token bucket modela un cubo con capacidad C que se rellena a un ritmo constante de R tokens por segundo. Cada petición consume un token; si el cubo está vacío, se rechaza.
La diferencia práctica es que la tasa se acota en cualquier ventana deslizante de tiempo, no solo en los límites artificiales de un reloj. El cubo sí permite ráfagas — hasta C peticiones seguidas si llevaba un rato sin usarse — pero nunca deja que el ritmo medio a largo plazo supere R, sea cual sea el momento en que empieces a medir.
La implementación habitual en un backend con varias instancias usa Redis, porque el contador tiene que ser compartido y la operación de comprobar-y-decrementar tiene que ser atómica:
-- ejecutado como script atómico en Redis (EVAL)
local tokens_key = KEYS[1]
local ts_key = KEYS[2]
local rate = tonumber(ARGV[1]) -- tokens por segundo
local capacity = tonumber(ARGV[2]) -- capacidad del cubo
local now = tonumber(ARGV[3])
local last_tokens = tonumber(redis.call("get", tokens_key)) or capacity
local last_ts = tonumber(redis.call("get", ts_key)) or now
local delta = math.max(0, now - last_ts)
local filled = math.min(capacity, last_tokens + delta * rate)
if filled < 1 then
return 0 -- sin tokens: rechazar
else
redis.call("set", tokens_key, filled - 1)
redis.call("set", ts_key, now)
return 1 -- hay token: dejar pasar
end
Ejecutarlo como script de Redis (en vez de varias llamadas GET/SET separadas) es lo que evita la condición de carrera: dos peticiones simultáneas no pueden leer el mismo estado y decrementar cada una por su cuenta, porque el script entero corre de forma atómica en el servidor de Redis.
Cómo responder cuando se supera el límite
La respuesta correcta es 429 Too Many Requests, acompañada de cabeceras que le digan al cliente qué hacer, no solo que ha fallado:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 12
Retry-After le dice al cliente cuántos segundos esperar antes de reintentar. Las cabeceras RateLimit-* — cada vez más estandarizadas entre proveedores — le permiten autorregularse antes de llegar al límite, consultando cuánto presupuesto le queda sin tener que provocar un 429 para averiguarlo.
Buenas prácticas
- Aplica el límite lo antes posible: en el gateway o el balanceador, antes de que la petición llegue a consumir conexión de base de datos o cómputo real. Rechazar tarde desperdicia justo el recurso que querías proteger.
- Diferencia límites por plan o tier: no todos los clientes necesitan el mismo presupuesto; un límite único penaliza a quien paga por más.
- Expón siempre las cabeceras de estado: un cliente que puede ver
RateLimit-Remainingse autorregula solo, en vez de descubrir el límite a base de errores. - No mezcles presupuestos entre endpoints muy distintos: una búsqueda cara y un healthcheck no deberían competir por el mismo cupo.
- Documenta el backoff esperado: si el cliente reintenta inmediatamente tras un 429 sin respetar
Retry-After, el límite no protege nada.
Con la identidad, los permisos, la duplicación y el volumen de tráfico bajo control, el siguiente problema típico aparece en cuanto una tabla crece: cómo devolver miles de resultados sin que la API — ni quien la consulta — se ahogue en una sola respuesta.