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.
Python code
46 linesimport 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
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
- Use a semaphore-based approach with `asyncio.Semaphore(capacity)` for a simpler but less precise limiter.
- 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
More from Concurrency & performance
- Benchmark list.append vs deque.append in Python medium
- Build a Python Performance Profiler That Generates Readable Reports medium
- Graceful Shutdown Executor Context Manager in Python medium
- How to Build a Producer-Consumer Pattern with asyncio.Queue in Python medium
- How to Cancel an asyncio Task with Graceful Cleanup in Python medium
- How to Convert Data in Parallel with ThreadPoolExecutor in Python easy
Keep learning
Related tutorials and quizzes for this topic.