Build a Rate Limiter Decorator in Python

This code defines a reusable rate limiter decorator that caps function calls within a sliding time window using a deque and monotonic time.

Easy Python 3.9+ Aug 9, 2026 Reliability & rate limiting 13 views 0 copies

Python code

32 lines
Python 3.9+
import time
from collections import deque


def rate_limiter(max_calls: int, period: float):
    calls = deque()

    def decorator(func):
        def wrapper(*args, **kwargs):
            now = time.monotonic()
            while calls and now - calls[0] >= period:
                calls.popleft()
            if len(calls) >= max_calls:
                raise RuntimeError(f"Rate limit exceeded: {max_calls} calls per {period}s")
            calls.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator


@rate_limiter(max_calls=2, period=1.0)
def fetch_data(item_id):
    return f"Data for item {item_id}"


if __name__ == "__main__":
    for item in range(6):
        try:
            print(fetch_data(item))
        except RuntimeError as e:
            print(f"{item}: {e}")
        time.sleep(0.3)

Output

stdout
Data for item 0
Data for item 1
2: Rate limit exceeded: 2 calls per 1.0s
Data for item 3
Data for item 4
5: Rate limit exceeded: 2 calls per 1.0s

How it works

The decorator keeps a deque of timestamps for recent calls. Each invocation uses time.monotonic() to get a reliable timestamp that isn't affected by system clock changes. Before adding a new call, it removes timestamps older than the period from the left of the deque, maintaining a sliding window. If the number of remaining calls equals or exceeds max_calls, it raises a RuntimeError; otherwise, it records the call and executes the function. This pattern is ideal for throttling external API requests or protecting internal resources from bursts.

Common mistakes

  • Using `time.time()` instead of `time.monotonic()` — system clock changes can break the window.
  • Forgetting to include the `@wraps` decorator, which loses the original function's metadata.
  • Not handling the `RuntimeError` gracefully in production code, causing crashes.
  • Sharing one limiter across multiple functions unintentionally, leading to over-throttling.

Variations

  1. Use `functools.lru_cache` or `threading.Lock` for thread-safe version in concurrent apps.
  2. Use `time.perf_counter` if you need higher precision for sub-millisecond windows.

Real-world use cases

  • Limiting outbound requests to a third‑party API that has per‑second quotas.
  • Throttling database write operations during high‑traffic spikes to prevent overload.
  • Enforcing per‑user rate limits on a web service endpoint to prevent abuse.

Sponsored

Run this sample

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

Open editor

More from Reliability & rate limiting

Related tutorials and quizzes for this topic.