Un limitador de tasa decide cuánto tráfico puede admitir un sistema. Sirve para proteger capacidad, repartir recursos entre clientes y aplicar cuotas. Cuando varias instancias reciben peticiones del mismo cliente, todas deben coordinar el consumo del presupuesto que comparten.
El diseño puede recorrerse en cinco pasos: definir requisitos, establecer la interfaz, elegir el algoritmo y su ubicación, coordinar el estado y resolver los compromisos de operación.
Requisitos
Los requisitos funcionales son identificar al cliente, seleccionar sus reglas y decidir si una petición puede continuar. Las reglas deben poder configurarse por usuario, aplicación, tenant o endpoint. Cuando el cliente agota su presupuesto, la API responde con HTTP 429 Too Many Requests y puede incluir Retry-After para indicar cuánto debe esperar.
La identidad debe proceder de información confiable, como el usuario autenticado o una API key validada. La dirección IP puede servir para ciertos límites, pero una red compartida puede agrupar a muchos usuarios bajo la misma dirección.
Los requisitos no funcionales son baja latencia, alta disponibilidad, tolerancia a fallos y un objetivo agregado de un millón de peticiones por segundo. Esa cifra es un objetivo de capacidad que debe validarse con la distribución real de clientes, reglas y tráfico. También hay que definir cuánto exceso de tráfico se tolera durante un fallo y qué latencia adicional puede introducir la decisión.
Entidades e interfaz
Las entidades centrales son el cliente identificado, la regla aplicable y la petición que consume presupuesto. El estado del limitador registra cuántos tokens quedan y cuándo se calculó por última vez su reposición.
Una interfaz conceptual puede expresarse así:
allow(subject, rule, cost) -> { allowed, retryAfter }subject identifica a quién se aplica el límite; rule define la capacidad y la tasa de reposición; cost indica cuántos tokens consume la operación. Una petición sencilla puede costar un token, mientras que una operación más cara puede consumir varios.
La lectura del presupuesto, la decisión y su actualización deben formar una sola operación atómica. Si dos gateways leen el mismo saldo y lo actualizan por separado, ambos podrían aprobar trabajo usando los mismos tokens.
Diseño de alto nivel
El limitador se ubica en el borde o en el API gateway, antes de enviar trabajo al servicio. Esto permite rechazar tráfico pronto y aplicar una política común a varios servicios. Las instancias del gateway comparten estado para coordinar las peticiones de un mismo cliente.
--- config: look: handDrawn --- flowchart TD client["Cliente autenticado"] --> gateway["API gateways<br/>Instancias 1 a N"] gateway -->|"Consultar y consumir presupuesto"| redis[("Redis primario<br/>Token Bucket + Lua")] redis -->|"allowed + retryAfter"| gateway gateway -->|"Petición admitida"| service["Servicio protegido"] gateway -->|"Presupuesto agotado"| rejected["HTTP 429<br/>Retry-After"] rejected --> client redis -.->|"Replicación asíncrona"| replica[("Réplica para failover")]
Cada bucket pertenece a un primario. Los gateways coordinan allí su consumo; la réplica permite recuperar el servicio ante un fallo y puede llevar retraso respecto al estado del primario.
El algoritmo elegido es Token Bucket. Cada bucket tiene una capacidad máxima B y repone tokens a una tasa r. Antes de decidir, calcula el saldo disponible:
tokens = min(B, previousTokens + r * elapsedSeconds)Si hay suficientes tokens, el limitador descuenta el coste y permite la petición. En caso contrario, la rechaza. El tiempo estimado hasta disponer de presupuesto es:
waitSeconds = (cost - tokens) / rEsta expresión supone r > 0 y un coste que cabe en el bucket. Para un Retry-After expresado en segundos enteros, la espera se redondea hacia arriba. Otras peticiones pueden consumir los tokens que se repongan durante ese intervalo.
Por ejemplo, un bucket con capacidad de 100 tokens y reposición de 10 tokens por segundo puede admitir una ráfaga inicial de hasta 100 peticiones de coste uno. Después de agotarse, recupera presupuesto a razón de 10 por segundo. La capacidad controla las ráfagas y la reposición controla el ritmo sostenido; el algoritmo permite ráfagas y puede superar 10 peticiones dentro de un segundo concreto.
Estado compartido en Redis
Redis mantiene el estado en memoria y permite que los gateways consulten el mismo bucket. Cada decisión ejecuta un script Lua que lee el saldo, calcula la reposición, verifica el coste y guarda el estado resultante.
Redis ejecuta el script de forma atómica respecto a otras operaciones en ese servidor. En Redis Cluster, todas las claves que utilice un mismo script deben pertenecer al mismo hash slot. Mantener el estado de cada bucket en una sola clave simplifica esa restricción. Una regla que combine presupuestos de varias claves necesita resolver explícitamente dónde se coordinan.
La reposición también requiere una referencia temporal consistente. Conviene calcularla con el reloj del servidor que posee el bucket y proteger el cálculo frente a retrocesos del reloj. Las claves pueden expirar tras un periodo de inactividad suficiente para que el bucket se haya llenado de nuevo; borrarlas antes podría devolver presupuesto prematuramente.
Los scripts deben ser breves: mientras se ejecutan ocupan el servidor y pueden aumentar la latencia de otras decisiones. El estado de los buckets merece una política de memoria que evite expulsarlos silenciosamente, porque perder un bucket puede reiniciar su presupuesto.
Escalabilidad
Redis Cluster distribuye los buckets entre shards. Esto permite repartir decisiones para muchos clientes y añadir capacidad horizontalmente. El número de shards debe elegirse a partir del rendimiento medido por decisión, incluyendo Lua, red, tamaño del estado y margen operativo.
Un cliente muy activo puede concentrar tráfico en una sola clave y saturar su shard. Esa clave sigue teniendo un único propietario aunque se añadan más shards. Para esos casos se pueden combinar límites locales con un presupuesto global, o asignar porciones del presupuesto a los gateways. Ambas estrategias requieren definir cómo se reparte y recupera el presupuesto, y qué desviación se acepta.
Disponibilidad y comportamiento ante fallos
Las réplicas permiten recuperar el servicio si falla un primario. Con replicación asíncrona, una promoción puede perder actualizaciones recientes del bucket y admitir tráfico por encima del límite previsto. Las decisiones que consumen tokens deben pasar por el primario; una réplica con lecturas atrasadas puede mostrar un saldo incorrecto.
También hay que decidir qué ocurre cuando el gateway pierde acceso al limitador:
- Fail open: permite continuar. Favorece la disponibilidad y puede aumentar la carga sobre los servicios protegidos.
- Fail closed: bloquea la operación. Protege el presupuesto, pero hace que la disponibilidad del servicio dependa del limitador.
- Límite local de emergencia: admite un volumen acotado por gateway mientras se recupera la coordinación. El exceso agregado depende de cuántos gateways estén activos y del presupuesto local que reciba cada uno.
La política puede variar por endpoint. Una operación costosa o sensible puede requerir un comportamiento distinto al de una lectura pública. HTTP 429 corresponde a una cuota agotada; si el sistema bloquea porque no puede consultar el limitador, HTTP 503 puede comunicar mejor la indisponibilidad temporal.
Latencia y distribución geográfica
El connection pooling reutiliza conexiones a Redis y evita pagar su establecimiento en cada petición. Los timeouts acotan cuánto espera el gateway antes de aplicar su política de fallo. La latencia debe medirse en percentiles como p95 y p99, incluyendo el salto de red y la espera para ejecutar el script.
Un timeout deja el resultado incierto: Redis puede haber descontado los tokens aunque su respuesta nunca llegue al gateway. Repetir la decisión puede descontar dos veces el mismo intento. La política de reintentos debe contemplar ese caso; si se requiere deduplicación, el diseño necesita identificar cada intento y recordar su decisión.
Acercar el estado al usuario reduce la latencia, pero obliga a decidir el alcance de la cuota. Un presupuesto global estricto necesita coordinar el consumo entre regiones. Los buckets independientes por región reducen esa coordinación; su presupuesto combinado debe repartirse para conservar una cuota global acotada.
La elección depende de qué importa para ese producto: exactitud global, capacidad de seguir operando durante una partición o rapidez de la decisión. Redis Cluster reparte estado entre shards, pero la coordinación de cuotas entre regiones necesita un diseño adicional.
Qué verificar
El diseño debe comprobarse con peticiones concurrentes para un mismo cliente, ráfagas, clientes que generan claves calientes y cambios de primario. También conviene simular pérdida de conectividad con Redis y observar qué ocurre con el tráfico admitido y la latencia del gateway.
Las señales centrales son peticiones admitidas y rechazadas, errores al consultar el limitador, tiempo de decisión y activaciones de la política de emergencia. Juntas permiten evaluar si el sistema protege el presupuesto y conserva el rendimiento bajo las condiciones para las que fue diseñado.