A high-performance, production-grade rate limiting library for Python. Volter implements Token Bucket and Sliding Window Log algorithms with thread-safe in-memory backends and distributed Redis backends. It also ships with drop-in FastAPI middleware.
from volter import TokenBucketLimiter
# Initialize a limiter: 10 burst capacity, refills at 1 token/sec
limiter = TokenBucketLimiter(capacity=10, refill_rate=1.0)
if limiter.allow("user:123"):
process_request()
else:
reject_with_429()Most rate limiter tutorials stop at a single, basic algorithm running in a single process. Volter provides production-ready implementations of multiple algorithms with genuinely different tradeoffs. It explicitly addresses concurrency race conditions—from in-process thread-safety to multi-process correctness across distributed app servers.
Quick Start • Architecture & Design • API Reference • Engineering Highlights (Why Volter?) • Development & Testing
-
Zero Third-Party Dependencies in the core package (
TokenBucketLimiter,SlidingWindowLimiter). -
Amortized
$O(1)$ Sliding Window Log performance utilizing a running total approach. - Lock-Free Read Paths for established keys using thread-safe double-checked locking in-memory.
- Distributed Multi-Process Safety utilizing atomic Redis Lua scripts.
-
Clock Drift Resilience via
time.monotonic()and Redis server-sideTIMEcommands. -
FastAPI Middleware that conforms to Python
Protocoltyping, working out-of-the-box with any limiter backend.
Install only what you need. Core limiters have zero dependencies. The redis and fastapi extras are loaded lazily, so you never pull in libraries you do not use.
# Core only: In-memory limiters (zero dependencies)
pip install volter
# In-memory + Redis-backed limiters
pip install volter[redis]
# In-memory + FastAPI middleware
pip install volter[fastapi]
# Full suite: In-memory, Redis, and FastAPI middleware
pip install volter[redis,fastapi]from volter import TokenBucketLimiter, SlidingWindowLimiter
# Token Bucket: Best for smoothing steady traffic with short burst capacity
# Allows bursts of up to 10 requests, refilling at 1 token per second
bucket = TokenBucketLimiter(capacity=10, refill_rate=1.0)
# Sliding Window Log: Best for strict interval counts (e.g., max 100 requests per minute)
window = SlidingWindowLimiter(capacity=100, window_size=60.0)
# Evaluate if a request should be processed
if bucket.allow("client_ip_or_user_id"):
# Process request...
passimport redis
from volter.redis_token_bucket import RedisTokenBucketLimiter
from volter.redis_sliding_window import RedisSlidingWindowLimiter
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# Token Bucket sharing state across multiple web-server processes
bucket = RedisTokenBucketLimiter(redis_client=r, capacity=100, refill_rate=10.0)
# Sliding Window Log sharing state across multiple web-server processes
window = RedisSlidingWindowLimiter(redis_client=r, capacity=100, window_size=60.0)Add rate limiting to your entire FastAPI application in three lines of code:
from fastapi import FastAPI
from volter import TokenBucketLimiter
from volter.fastapi_middleware import RateLimitMiddleware
app = FastAPI()
limiter = TokenBucketLimiter(capacity=5, refill_rate=1.0)
# Out-of-the-box middleware (rejects with a 429 status code and 'Retry-After: 1' header)
app.add_middleware(RateLimitMiddleware, limiter=limiter)You can customize the rate limit key (e.g., using an API key or route instead of client IP) using a custom key_func:
app.add_middleware(
RateLimitMiddleware,
limiter=limiter,
key_func=lambda request: request.headers.get("X-API-Key", "anonymous"),
)Volter is designed with performance, correctness, and low resource overhead in mind.
Instead of protecting the entire limiter database with a single global lock—which would serialize all incoming API requests and severely bottle-neck throughput—Volter uses two-level locking:
- A global dictionary-level lock (
_buckets_lock/_logs_lock) is used only when initializing a state tracker for a new key. - A double-checked locking pattern is used: if the key already exists, we retrieve it lock-free. We only acquire the global lock if the key is absent.
- A fine-grained, per-key lock (
bucket.lock/log.lock) is acquired to perform the read-compute-write sequence for that specific user.
Thus, concurrent requests for user:123 never block concurrent requests for user:456.
A naive sliding window log recalculates the active count on every request by iterating over the entire queue of timestamps—an
Volter optimizes this to amortized current_sum) for each key. When a request arrives, we evict only the expired records from the front of the queue and decrement our running total accordingly. Each evaluation is
- In-Memory: Standard clock mechanisms like
time.time()represent wall-clock time and can jump backward or forward (due to NTP syncs, VM suspends, or manual clock alterations), breaking rate-limiting math. Volter exclusively usestime.monotonic()in-memory, ensuring elapsed time calculations are guaranteed to be forward-only. - Distributed (Redis): Different application servers can suffer from clock skew relative to each other. To avoid this, Volter's Redis scripts call Redis's own
TIMEcommand to retrieve the central Redis clock timestamp, neutralizing clock-skew issues completely.
Implements the Token Bucket algorithm. Best for most APIs. Allows a burst of up to capacity requests, smooths down to a steady rate of refill_rate per second.
- Constructor:
TokenBucketLimiter(capacity: int, refill_rate: float, max_idle: float = 0.0)
capacity: Maximum number of tokens the bucket can hold.refill_rate: How many tokens are added to the bucket per second.max_idle: How long (in seconds) a key can go untouched before its internal state is evicted from memory to save space. Defaults to(capacity / refill_rate) * 2.
- Methods:
allow(key: str, tokens_requested: float = 1.0) -> bool: ReturnsTrueif the bucket contains at leasttokens_requestedtokens. If so, consumes them.
Implements the Sliding Window Log algorithm. Best for precise limit enforcement over strict intervals.
- Constructor:
SlidingWindowLimiter(capacity: int, window_size: float, max_idle: float = 0.0)
capacity: Total allowed weighted units inside the sliding window.window_size: The duration of the sliding window in seconds.max_idle: How long (in seconds) a key can remain idle before eviction. Defaults towindow_size * 2.
- Methods:
allow(key: str, tokens_requested: float = 1.0) -> bool: ReturnsTrueif addingtokens_requestedkeeps the total weight withincapacityfor the sliding window.
Distributed Token Bucket. Utilizes a Redis hash storing tokens and last_refill attributes.
- Constructor:
RedisTokenBucketLimiter(redis_client: redis.Redis, capacity: int, refill_rate: float, ttl: int = 3600, key_prefix: str = "volter:tb")
redis_client: Aredis-pyclient instance.capacity: Maximum capacity of the token bucket.refill_rate: Refill rate in tokens per second.ttl: Time-to-live in seconds for idle keys. Enables automated self-cleanup of stale keys in Redis.key_prefix: Prefix added to keys inside Redis to avoid namespace collisions.
- Methods:
allow(key: str, tokens_requested: float = 1.0) -> bool: Atomic check-and-consume via evaluated server-side Lua script.
Distributed Sliding Window. Utilizes a Redis Sorted Set (ZSET) where elements represent individual requests.
- Constructor:
RedisSlidingWindowLimiter(redis_client: redis.Redis, capacity: int, window_size: float, ttl: int | None = None, key_prefix: str = "volter:sw")
redis_client: Aredis-pyclient instance.capacity: Exact request capacity allowed inside the window.window_size: Size of the sliding window in seconds.ttl: Redis key TTL. Defaults toint(window_size) + 1.key_prefix: Redis key prefix.
- Methods:
allow(key: str) -> bool: Atomic sliding-window calculation using a Redis Sorted Set.
An ASGI middleware designed to catch and enforce rate limits globally or on a custom key basis.
- Constructor:
RateLimitMiddleware(app: ASGIApp, limiter: _Limiter, key_func: Callable[[Request], str] = _default_key_func, tokens_requested: float = 1.0)
app: The ASGI application instance.limiter: Any rate limiter implementation conforming to the_Limiterprotocol (must implementallow(key: str, tokens_requested: float) -> bool).key_func: A callable accepting a FastAPIRequestand returning astrused as the rate-limiting key. Defaults to returning the client's host IP.tokens_requested: The default weight/token cost applied to requests intercepted by this middleware.
This library was built with a deep understanding of standard distributed system failures, concurrency issues, and efficiency optimizations.
In a distributed setup with multiple application servers, rate limiting requires atomicity. A naive implementation using a sequence of commands (e.g., GET -> compute -> SET) creates a classic check-then-act race condition where concurrent processes read stale states and bypass limits.
Volter solves this by packaging the logic inside indivisible Lua scripts executed on the Redis server:
- Since Redis is single-threaded, a Lua script runs completely uninterrupted as a single command, neutralizing race conditions without requiring global distributed locks.
- We chose Lua scripts over standard
MULTI/EXECpipelines becauseMULTI/EXECcannot branch mid-transaction based on read results without utilizing expensive optimistic locking retry loops (WATCHcommands).
In the RedisSlidingWindowLimiter, we store requests inside a Redis sorted set (ZSET). Redis ZSETs require members to be unique.
- If we stored raw timestamps as the ZSET members, two concurrent requests arriving within the exact same microsecond would collision, overwriting each other and leading to an undercount (allowing clients to bypass limits).
- Volter solves this by generating a unique UUID4 in Python for each request and storing the UUID as the member, while the epoch timestamp is stored as the ZSET score. This ensures every single request occupies a distinct node in Redis's skip-list.
- Why generate UUIDs in Python instead of inside Lua? Redis replication requires Lua scripts to be strictly deterministic. Using random-number or UUID generators inside a Lua script is restricted as it breaks replication safety. Generating the UUID in Python keeps the Lua script deterministic and replication-safe.
To guarantee that the concurrency safety mechanisms are production-ready, Volter avoids testing concurrency purely with greenlets or thread-pools.
Our Redis integration tests spin up true operating-system processes (multiprocessing.Process), each holding its own independent Redis connection. These processes hammer the Redis-backed limiters concurrently. The test suite asserts that the exact specified capacity limit is enforced to the single request, validating Volter's thread/process safety guarantees in high-concurrency environments.
-
Passive In-Memory Eviction: Memory cleanup is done on-demand during subsequent calls to
.allow(). If a key becomes completely idle and is never called again, it remains in memory until a sweep is triggered. A proactive, background worker sweep is a planned enhancement. -
Static Retry-After Header: The FastAPI middleware currently returns a static
Retry-After: 1header. Calculating and returning the exact millisecond-precision wait time for each algorithm is on the roadmap. -
Weighted Requests for Redis Sliding Window: The Redis sliding window currently counts requests with a static weight of
$1$ . Encoding weights inside ZSET members to support varying weights is a natural next step.
Volter utilizes uv for modern, lightning-fast Python package and dependency management.
# Sync all virtual environment and dev dependencies
uv sync --all-extrasThe test suite consists of unit tests, in-memory concurrency tests, and Redis integration tests.
# Run the full test suite
uv run pytestTo run the Redis-backed integration tests, you must have a local Redis instance running. You can easily boot one up using Docker:
# Launch local Redis
docker run -d --name volter-redis -p 6379:6379 redis:7-alpine
# Run the Redis tests
uv run pytest -v tests/test_redis_token_bucket.py tests/test_redis_sliding_window.py