Token Bucket en C#: Guía Completa

Introducción: control de tráfico

El patrón Token Bucket es un mecanismo de control de tasa diseñado para regular el consumo de recursos de forma flexible. Su propósito es limitar el número de solicitudes permitidas en un intervalo, pero sin imponer un flujo rígido e inflexible. En lugar de bloquear toda variación, permite ráfagas controladas, algo especialmente útil en APIs donde el tráfico real no es uniforme. La idea central es simple: los tokens representan permisos. Para procesar una solicitud, el sistema debe “gastar” un token. Los tokens se regeneran a un ritmo constante hasta un máximo, lo que define la capacidad total de la ráfaga.

En términos operativos, Token Bucket ayuda a proteger servicios de sobrecarga y abuso, a la vez que evita penalizar patrones de uso legítimos. Un cliente puede acumular tokens durante periodos de baja actividad y luego consumirlos rápidamente en un pico breve. Esto es útil cuando un usuario necesita ejecutar una operación puntual con múltiples llamadas o cuando una aplicación sincroniza datos en ráfagas. Sin Token Bucket, ese patrón sería visto como un exceso, aunque sea un uso válido.

La diferencia clave frente a otros mecanismos de rate limiting es la elasticidad. Con Token Bucket, el límite no es solo “X solicitudes por minuto”, sino “X solicitudes por minuto en promedio, con capacidad de ráfaga hasta Y”. Este matiz es importante para diseñar experiencia de usuario y estabilidad del servicio. Si el límite es demasiado rígido, los clientes legítimos sufren. Si es demasiado flexible, el servicio puede saturarse. Token Bucket permite ajustar ambos componentes de forma separada.

El patrón también se utiliza como una medida de seguridad. Cuando un servicio está expuesto públicamente, limitar el tráfico reduce el impacto de ataques de denegación de servicio o de bots que intentan saturar el endpoint. Aunque no reemplaza defensas específicas contra DDoS, sí añade una capa de protección contra abuso normalizado. Además, ayuda a mantener una distribución justa de recursos entre clientes, evitando que uno solo monopolice el sistema.

En C#, el patrón puede implementarse en memoria para servicios individuales, o en un almacenamiento distribuido cuando el límite debe aplicarse de forma global en un clúster. Esa decisión depende de la arquitectura: en un servicio monolítico o con una sola instancia, un bucket en memoria es suficiente. En un escenario distribuido con múltiples instancias, un bucket local permite ráfagas por instancia, lo cual puede multiplicar el límite real. Por eso, en clústeres se suele usar un almacenamiento centralizado para mantener consistencia.

En resumen, Token Bucket es una solución equilibrada para controlar tráfico sin eliminar la flexibilidad. Su diseño es simple, pero su impacto es grande: reduce saturación, mejora la estabilidad y permite políticas de uso razonables. El patrón no se limita a “bloquear tráfico”, sino a administrar el consumo de recursos de manera justa y predecible, alineada con la realidad del tráfico moderno.

Token Bucket
Prompt: bucket with tokens, minimal style.

1. Naturaleza: balde de agua

La analogía del balde de agua captura de forma intuitiva el funcionamiento del patrón. Imagina un balde con capacidad limitada. Un grifo lo llena a un ritmo constante, y un usuario puede extraer agua cuando lo necesite. Si el usuario extrae más rápido de lo que se llena, el balde se vacía y debe esperar a que se reponga. Si el usuario no extrae agua por un tiempo, el balde se llena hasta su capacidad máxima, almacenando una “reserva” para usos futuros. Este comportamiento es casi idéntico a cómo funciona Token Bucket.

En el mundo natural, esta analogía también se ve en sistemas de almacenamiento de energía o recursos. Un organismo puede acumular reservas durante periodos de abundancia, y consumirlas rápidamente durante periodos de alta demanda. Sin embargo, esas reservas tienen un límite; no puede acumularse energía indefinidamente. Esta dinámica de acumulación y gasto representa la lógica de “tokens” y “capacidad de ráfaga”. El sistema permite un pico temporal si hubo tiempo para acumular recursos, pero impone un límite para evitar un uso excesivo permanente.

La clave de la analogía es el ritmo fijo de reposición. El grifo no acelera solo porque el usuario tenga más necesidad. En Token Bucket, la tasa de llenado es constante y define el promedio de solicitudes permitido. Esto protege al sistema de cargas sostenidas excesivas, mientras sigue permitiendo flexibilidad en el corto plazo. En términos de diseño, es como decir: “Puedes gastar más ahora, pero solo si ahorraste antes”.

Otro elemento importante es la capacidad del balde. Si el balde es pequeño, las ráfagas serán cortas. Si es grande, las ráfagas pueden ser mayores, pero también aumenta el riesgo de saturación si muchos usuarios aprovechan esa capacidad al mismo tiempo. En Token Bucket, la capacidad del bucket se define explícitamente y representa el máximo de tokens acumulables. Ajustar este valor es una decisión de negocio: se decide cuánta “elasticidad” se permite antes de considerar abuso o riesgo para el sistema.

La analogía también ayuda a entender el comportamiento frente a picos simultáneos. Si varios usuarios comparten el mismo balde, un usuario podría consumir la mayoría de los tokens y dejar a los demás sin recursos. Por eso, en sistemas reales se suelen usar buckets por cliente, por IP o por API key. Es una forma de garantizar equidad. En la naturaleza, sería como tener un balde para cada individuo en lugar de uno compartido para toda la comunidad.

Finalmente, el balde también ilustra el concepto de “espera natural”. Cuando no hay tokens, no hay acceso, pero el sistema no se “rompe”; solo se espera a que se repongan. Esto se traduce en la respuesta 429 Too Many Requests en APIs. El patrón no es punitivo, sino regulador. Introduce una dinámica natural de consumo y reposición que evita la saturación y permite un uso razonable del recurso.

Balde
Prompt: water bucket fill rate, soft illustration.

2. Mundo Real: APIs públicas

En APIs públicas, Token Bucket es una herramienta esencial para mantener disponibilidad y equidad. Imagina una API que permite 100 solicitudes por minuto con una capacidad de ráfaga de 20. Esto significa que, en promedio, el cliente no puede superar 100 por minuto, pero si estuvo inactivo durante un periodo, puede realizar hasta 20 solicitudes rápidas en un corto intervalo. Esta flexibilidad es crucial en escenarios donde los clientes realizan sincronizaciones puntuales o acciones en lote que no representan un abuso, sino un patrón legítimo.

Las plataformas que ofrecen APIs a terceros suelen aplicar límites por API key, por usuario o por IP. Token Bucket permite que estos límites sean razonables. Por ejemplo, un cliente que realiza llamadas intermitentes no debería ser penalizado con un límite rígido que no considera periodos de inactividad. Con Token Bucket, esos periodos se convierten en “tokens acumulados” que permiten pequeños picos. Esto mejora la experiencia de integradores sin comprometer la estabilidad general.

Otro ejemplo común es el de servicios financieros o de comercio electrónico, donde ciertas operaciones se ejecutan en lote. Un sistema de conciliación puede hacer decenas de llamadas seguidas al cierre del día, pero casi ninguna el resto del tiempo. Un límite rígido por minuto podría bloquear ese proceso. Token Bucket permite que el sistema acumule tokens durante horas y los use en una ráfaga controlada. Esto mantiene el servicio protegido frente a abuso sostenido, pero no bloquea operaciones legítimas.

En microservicios, Token Bucket también se usa internamente. Un servicio que llama a otro puede utilizar un bucket para evitar sobrecargar una dependencia. Esto es útil en cascadas de fallos: si un servicio downstream comienza a responder lento, el bucket ayuda a limitar llamadas para evitar saturación. Aunque este uso se relaciona con resiliencia, en entornos donde los límites son parte de un contrato interno, Token Bucket se convierte en un mecanismo de seguridad operativa.

El patrón también se usa para proteger rutas específicas. Por ejemplo, un endpoint de login puede tener un bucket más estricto para evitar ataques de fuerza bruta, mientras que un endpoint de lectura pública puede tener límites más laxos. Esta diferenciación de límites es esencial para mantener seguridad sin afectar la experiencia general. El diseño correcto incluye umbrales distintos según criticidad y costo de la operación.

Además, el contexto distribuido importa. En un sistema con múltiples instancias detrás de un balanceador, un bucket local por instancia puede multiplicar el límite real. Por eso, en escenarios donde el límite debe ser global, se usa un almacenamiento compartido (por ejemplo, Redis) para sincronizar tokens. Esa decisión afecta latencia y complejidad, pero es necesaria si se requiere una política consistente. En resumen, Token Bucket en el mundo real es tanto una herramienta de seguridad como de gobernanza de recursos.

API
Prompt: API rate limiting tokens, flat infographic.

3. Implementación en C#: Código Paso a Paso

if (!bucket.TryConsume(1)) return TooManyRequests();

Implementar Token Bucket en C# requiere definir tres parámetros clave: la capacidad del bucket, la tasa de reposición y la unidad temporal. La capacidad define cuántos tokens pueden acumularse. La tasa de reposición define cuántos tokens se agregan por unidad de tiempo. La unidad temporal define la granularidad del cálculo. Con estos elementos, se puede construir un componente que, al recibir una solicitud, intente consumir un token. Si hay tokens disponibles, se permite la solicitud; si no, se responde con un límite excedido.

Una implementación simple en memoria puede mantener el número de tokens actuales y el último momento en que se actualizó el bucket. Cada vez que llega una solicitud, se calcula cuántos tokens deben añadirse según el tiempo transcurrido, respetando el límite de capacidad. Luego se intenta consumir un token. Esta lógica es determinista y eficiente. Sin embargo, debe ser thread-safe si el servicio es concurrente. En C#, esto puede lograrse con bloqueos ligeros o con mecanismos de sincronización adecuados para evitar condiciones de carrera.

En un entorno distribuido, el problema se vuelve más complejo. Si el servicio tiene múltiples instancias, cada una con su propio bucket en memoria, el límite real se multiplica por el número de instancias. Para evitarlo, se necesita un almacenamiento compartido. Redis es una opción común, porque permite operaciones atómicas y expiraciones. La lógica del bucket se implementa en una estructura compartida y los tokens se consumen de forma global. Este enfoque garantiza que el límite se respete a nivel de clúster, pero introduce latencia y dependencia en un servicio externo.

Otra decisión de implementación es dónde aplicar el rate limiting. Puede implementarse como middleware en la capa HTTP, de forma que todas las solicitudes pasen por el bucket antes de llegar a la lógica de negocio. Esto simplifica la integración y asegura una política uniforme. También puede aplicarse por endpoint específico, con buckets independientes según la criticidad de la operación. En .NET, esto puede integrarse con políticas de endpoints o filtros. El objetivo es que la política sea explícita y mantenible.

La implementación también debe manejar correctamente el tiempo. Usar el reloj del sistema es suficiente, pero hay que considerar la precisión y evitar errores de overflow al calcular tokens acumulados. En sistemas de alta concurrencia, es recomendable usar operaciones atómicas para actualizar tokens y tiempo. También es importante evitar que una solicitud con un timestamp incorrecto restablezca el bucket. Por eso, la lógica debe asegurar que el tiempo no retrocede.

Finalmente, el comportamiento frente a límites excedidos debe ser coherente. El servidor debe responder con un código apropiado (como 429) e incluir, si es posible, información sobre cuándo volver a intentar. Esto ayuda a clientes bien implementados a respetar el límite. En resumen, la implementación en C# no es solo “consumir un token”, sino diseñar una política de control de tráfico robusta, consistente y segura, adaptada al entorno (monoinstancia o distribuido) y a los requisitos del servicio.

4. Token Bucket vs Leaky Bucket

Token Bucket y Leaky Bucket son patrones similares en el objetivo, pero diferentes en el comportamiento. Token Bucket permite ráfagas controladas porque los tokens pueden acumularse. Si un cliente estuvo inactivo, puede consumir varios tokens de una sola vez. Leaky Bucket, en cambio, procesa solicitudes a un ritmo constante, como un balde con un pequeño agujero que gotea a velocidad fija. Esto suaviza el flujo y elimina picos, independientemente de cuánto tiempo haya estado inactivo el cliente.

La diferencia práctica es importante. Token Bucket es más flexible para clientes con tráfico irregular, mientras que Leaky Bucket es más estricto y predecible. Si se quiere evitar cualquier pico, Leaky Bucket es más apropiado. Si se quiere permitir picos breves y legítimos, Token Bucket es mejor. En APIs públicas, Token Bucket suele ser preferido porque los clientes reales tienden a tener patrones de uso intermitentes. En sistemas críticos donde el flujo debe ser constante (por ejemplo, ciertos pipelines), Leaky Bucket puede ser más adecuado.

Otra diferencia es cómo se percibe la latencia. En Token Bucket, si hay tokens, las solicitudes pasan inmediatamente. En Leaky Bucket, las solicitudes pueden “esperar” para ser liberadas a ritmo fijo. Esto puede introducir una latencia adicional, pero garantiza un flujo uniforme. En aplicaciones sensibles a la latencia, Token Bucket puede ser más aceptable. En aplicaciones sensibles al throughput constante, Leaky Bucket es más estable.

Ambos patrones pueden implementarse con almacenamiento en memoria o distribuido, pero la semántica cambia. En Leaky Bucket, el tamaño de la cola y la velocidad de fuga determinan cuánto atraso puede tolerar el sistema. En Token Bucket, la capacidad y la tasa determinan cuánto puede burstear un cliente. Estas diferencias afectan cómo se diseña el SLA y cómo se comunica a los clientes. Por ejemplo, un límite “100 req/min con ráfagas de 20” es una descripción de Token Bucket; en Leaky Bucket se diría “100 req/min con cola máxima de 20 y procesamiento uniforme”.

También hay diferencias en la interacción con otros mecanismos de control. Token Bucket se combina bien con políticas de cuotas diarias o mensuales, porque permite balancear picos y mantener un promedio. Leaky Bucket se combina bien con mecanismos de smoothing cuando se necesita estabilidad de carga. Un sistema puede incluso usar ambos: Token Bucket para límites por cliente y Leaky Bucket para estabilizar el tráfico agregado.

En resumen, Token Bucket es un patrón de “elasticidad controlada” y Leaky Bucket es un patrón de “flujo constante”. La elección depende de la naturaleza del tráfico y de la tolerancia al pico. Comprender estas diferencias evita aplicar un mecanismo demasiado estricto o demasiado permisivo, y ayuda a diseñar políticas que sean seguras y razonables para los usuarios.

5. Diagrama UML

En el diagrama UML de Token Bucket se suelen representar tres componentes principales: el consumidor de solicitudes, el bucket y un reloj o generador de tokens. El consumidor envía solicitudes al bucket. El bucket valida si hay tokens disponibles; si los hay, autoriza la solicitud y reduce el conteo. El generador de tokens, basado en el tiempo, repone tokens hasta la capacidad máxima. Este diagrama refleja la lógica central del patrón: la autorización depende de un recurso (token) que se regenera con el tiempo.

El UML puede mostrar el bucket como una clase con atributos como `capacity`, `tokens` y `lastRefill`. Los métodos principales son `Refill()` y `TryConsume(n)`. `Refill()` calcula cuántos tokens deben añadirse en función del tiempo transcurrido, y `TryConsume(n)` verifica si hay tokens suficientes para permitir la operación. En una implementación distribuida, se puede representar un repositorio externo (por ejemplo, un almacén compartido) que guarda el estado del bucket y permite operaciones atómicas.

El diagrama de flujo complementario describe el ciclo de una solicitud: el cliente llega, el bucket calcula tokens disponibles, se decide permitir o rechazar, y se responde en consecuencia. En caso de rechazo, el flujo termina con un error de rate limit. En caso de aceptación, la solicitud continúa hacia la lógica de negocio. Este flujo ayuda a entender que el control ocurre antes del procesamiento, lo que evita consumir recursos del servicio cuando el límite ya fue excedido.

En diagramas más completos, se pueden incluir diferentes buckets según el criterio de limitación: por usuario, por IP, por API key, o por endpoint. Cada uno se representa como una instancia diferente del bucket, con sus propios parámetros. Esto permite visualizar una política de rate limiting más compleja, donde un mismo request debe pasar múltiples controles. El diagrama también puede incluir un componente de configuración que define capacidades y tasas, mostrando cómo las políticas pueden cambiar sin modificar el código.

Si se considera el uso distribuido, el UML puede incluir un componente de sincronización o un cache distribuido. Esto sirve para representar que la lógica de actualización y consumo es global y no local a una sola instancia. En sistemas críticos, esta distinción es importante porque define el límite real. El diagrama ayuda a los equipos a comprender dónde está el estado compartido y cómo se mantiene la consistencia.

En resumen, el UML del patrón Token Bucket no es solo un dibujo conceptual, sino una herramienta para identificar puntos críticos: dónde se actualizan tokens, cómo se garantiza concurrencia, y dónde se define la política. Visualizarlo ayuda a evitar implementaciones incompletas, como buckets que no respetan la capacidad o que generan tokens de manera incorrecta. Un buen diagrama comunica la intención y la mecánica del patrón de forma clara.

UML Token Bucket
Prompt: UML token bucket, clean vector.
Flow Token Bucket
Prompt: token bucket flow diagram, minimal infographic.

⚠️ Cuándo NO Usar Token Bucket

Token Bucket no es la solución correcta para todos los casos. Si la prioridad es un flujo completamente constante, sin picos ni variaciones, Token Bucket puede ser demasiado permisivo. En ese escenario, un mecanismo como Leaky Bucket o un control de cola con procesamiento uniforme es más adecuado. Token Bucket permite ráfagas por diseño; si esas ráfagas son inaceptables, el patrón no cumple el requisito.

Otro caso problemático es cuando no se puede mantener o sincronizar el estado del bucket. En un entorno distribuido, el bucket debe ser compartido si se quiere un límite global real. Si no hay una forma de mantener ese estado (por restricciones técnicas, latencia o costos), el patrón pierde efectividad porque cada instancia aplicará su propio límite. Esto puede llevar a límites reales mucho más altos que los deseados, lo que es peligroso en APIs públicas o sistemas sensibles.

También puede no ser adecuado cuando el tráfico no es el recurso principal a controlar. Si el problema real es el consumo de CPU, memoria o recursos externos, un simple rate limit por solicitudes puede ser insuficiente. En esos casos, es mejor usar mecanismos basados en costo o en recursos consumidos, que ponderen el impacto real de cada operación. Token Bucket trata todas las solicitudes iguales, a menos que se implemente un modelo de consumo variable por token. Si los costos son muy heterogéneos, el patrón puede necesitar adaptaciones.

En algunos sistemas, la experiencia de usuario puede verse afectada si el bucket está mal configurado. Si los límites son demasiado bajos, los clientes legítimos se verán bloqueados y el sistema parecerá inestable. Si son demasiado altos, el bucket no ofrecerá protección real. Por eso, usar Token Bucket requiere monitoreo y ajuste continuo. En sistemas sin capacidad de observabilidad, el patrón puede ser más un problema que una solución, porque se aplica sin datos reales.

Finalmente, Token Bucket no reemplaza otras medidas de seguridad. No protege contra ataques sofisticados ni sustituye mecanismos de autenticación o autorización. Si se usa como única defensa, se crea una falsa sensación de seguridad. El patrón debe ser parte de un conjunto de controles: autenticación, autorización, monitoreo y protección de infraestructura. Usarlo donde no aporta valor o donde introduce complejidad sin beneficios es una mala decisión.

En resumen, Token Bucket es útil cuando se necesita limitar tasas con flexibilidad, pero no cuando se requiere flujo constante, consistencia global estricta sin infraestructura compartida, o cuando el costo de operación supera el beneficio. La clave está en entender el contexto y elegir el patrón adecuado.

💪 Ejercicio

Diseña una política de Token Bucket para un API que ofrece búsqueda y creación de recursos. Primero, define dos tipos de operaciones: lectura y escritura. Decide límites distintos para cada una. Por ejemplo, una lectura puede ser más frecuente, mientras que una escritura es más costosa. Define para cada operación una tasa de reposición y una capacidad de ráfaga. Explica por qué esos valores son razonables según el costo de cada operación y el perfil esperado de uso.

En la segunda fase, define el criterio de aplicación del bucket. ¿Será por usuario autenticado, por API key o por IP? Justifica tu elección. Si es por usuario, explica cómo se obtiene el identificador y qué ocurre con usuarios no autenticados. Si es por IP, analiza los riesgos de falsos positivos en redes compartidas. Esta elección es clave, porque afecta la equidad y la efectividad del control.

En la tercera fase, diseña el manejo de límites excedidos. Define la respuesta del servidor, el código HTTP y los headers informativos. Considera incluir información sobre cuándo se repondrá el siguiente token. Esto ayuda a que los clientes se adapten en lugar de reintentar de forma agresiva. También define qué métricas quieres registrar: número de rechazos, tasas promedio, picos de consumo y distribución por cliente.

En la cuarta fase, piensa en un entorno distribuido. Si tu API corre en varias instancias, ¿cómo garantizas que el límite sea global? Describe si usarías un almacén compartido y qué implicaciones tendría en latencia. Si decides usar buckets locales por instancia, explica cómo ajustarías los límites para no multiplicar el máximo permitido. Esta reflexión es esencial para diseños reales.

Finalmente, define un plan de ajustes. ¿Cómo decidirías si el límite es demasiado alto o bajo? ¿Qué señales del sistema te indicarían que debes cambiar la política? Describe un proceso de evaluación con datos reales. El ejercicio se completa cuando tienes una política escrita, criterios de aplicación, manejo de errores y un plan de observabilidad. Eso representa un diseño completo, no solo un límite numérico.

Conclusión

Token Bucket es un patrón simple pero poderoso para controlar tráfico sin sacrificar la flexibilidad necesaria en sistemas modernos. Permite un promedio controlado con ráfagas aceptables, ofreciendo una experiencia más justa para clientes legítimos y una protección básica contra abuso. Su valor no está solo en bloquear solicitudes, sino en regular el consumo de recursos de forma predecible y sostenible.

En APIs públicas y sistemas distribuidos, esta capacidad de balancear protección y elasticidad es crucial. Un límite rígido puede generar frustración y fallas operativas; un límite demasiado permisivo puede causar saturación. Token Bucket aporta un mecanismo paramétrico para ajustar ambos extremos de manera razonable. La capacidad y la tasa de reposición son knobs que se pueden calibrar según datos reales, evitando decisiones arbitrarias.

La implementación correcta requiere atención a la concurrencia, al manejo del tiempo y a la coherencia en entornos distribuidos. En C#, esto se traduce en un diseño cuidadoso de estado, sincronización y middleware de control. Si el sistema se escala horizontalmente, el bucket debe ser global o los límites se multiplican. Esa decisión es arquitectónica, no solo técnica. Por eso, el patrón también fuerza a pensar en la topología del sistema y en la observabilidad necesaria.

También es importante recordar que Token Bucket no reemplaza otras medidas de seguridad. Es una capa más en un sistema de defensa en profundidad. Funciona bien junto a autenticación, autorización, monitoreo y políticas de uso. Usado correctamente, reduce la superficie de ataque y protege recursos críticos, pero siempre requiere configuración y ajuste continuo.

En conclusión, Token Bucket ofrece un equilibrio práctico entre estabilidad y experiencia. Su modelo es intuitivo, su implementación es accesible y su impacto es tangible. Cuando se aplica con criterios claros y con buena observabilidad, se convierte en una herramienta esencial para mantener servicios robustos y justos frente a tráfico variable y potencialmente hostil.