How to Implement a Token Bucket Rate Limiter with asyncio in Python

This code implements a thread-safe token bucket rate limiter for asyncio, allowing you to limit the rate of async tasks or API calls.

Medium Python 3.7+ Aug 9, 2026 Concurrency & performance 15 views 0 copies

Python code

46 lines
Python 3.7+
import asyncio
import time


class TokenBucket:
    def __init__(self, rate_per_second, capacity):
        self.rate = rate_per_second
        self.capacity = capacity
        self.tokens = capacity
        self.last_refill = time.monotonic()
        self.lock = asyncio.Lock()

    async def acquire(self):
        async with self.lock:
            now = time.monotonic()
            elapsed = now - self.last_refill
            self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
            self.last_refill = now

            if self.tokens < 1:
                wait_time = (1 - self.tokens) / self.rate
                await asyncio.sleep(wait_time)
                self.tokens = 0
                self.last_refill = time.monotonic()
            else:
                self.tokens -= 1


async def worker(name, bucket, delay):
    for i in range(3):
        await bucket.acquire()
        print(f"Worker {name}: task {i} at {time.time():.2f}")
        await asyncio.sleep(delay)


async def main():
    bucket = TokenBucket(rate_per_second=2, capacity=2)
    tasks = [
        asyncio.create_task(worker("A", bucket, 0.3)),
        asyncio.create_task(worker("B", bucket, 0.3)),
    ]
    await asyncio.gather(*tasks)


if __name__ == "__main__":
    asyncio.run(main())

Output

stdout
Worker A: task 0 at 1234567890.12
Worker B: task 0 at 1234567890.12
Worker A: task 1 at 1234567890.62
Worker B: task 1 at 1234567890.62
Worker A: task 2 at 1234567891.12
Worker B: task 2 at 1234567891.12

How it works

The token bucket algorithm allows bursts up to capacity tokens but limits the average rate to rate tokens per second. Tokens are added continuously based on elapsed time, using time.monotonic() to avoid clock jumps. The asyncio.Lock ensures that concurrent coroutines don't corrupt the token count. When tokens are insufficient, the coroutine sleeps for the required wait time before proceeding. This pattern is ideal for rate-limiting asynchronous operations like API calls or database queries.

Common mistakes

  • Using `time.time()` instead of `time.monotonic()` can cause token over-refill if the system clock is adjusted.
  • Forgetting to use a lock in a shared bucket when multiple tasks acquire tokens can lead to race conditions.
  • Setting `capacity` too low may cause unnecessary throttling even when the average rate is within limits.

Variations

  1. Use a semaphore-based approach with `asyncio.Semaphore(capacity)` for a simpler but less precise limiter.
  2. Implement a distributed token bucket (e.g., with Redis) for rate limiting across multiple processes or machines.

Real-world use cases

  • Rate-limit outgoing HTTP requests to a third-party API to respect its rate limits.
  • Throttle database operations in an async web server to prevent overload.
  • Control the rate of message publishing to a queue or event stream to avoid overwhelming consumers.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Concurrency & performance

Related tutorials and quizzes for this topic.