A distributed rate limiter built with FastAPI, Redis, Lua, and the Token Bucket algorithm.
The project demonstrates how an API can control request traffic, prevent excessive requests, and maintain shared rate-limit state across multiple application instances .
- Token Bucket rate-limiting algorithm
- Redis-backed shared state
- Atomic operations using Lua
- Per-client rate limiting
- Per-endpoint rate-limit policies
- FastAPI middleware
- HTTP 429 responses
- Redis running in Docker
Client
|
v
FastAPI
|
v
Rate Limit Middleware
|
v
Identify Client + Policy
|
v
Redis + Lua Script
|
+---- Allowed ----> API Endpoint ----> Response
|
+---- Rejected ---> HTTP 429
When a request reaches the application, the rate-limit middleware runs before the request reaches the API endpoint.
The middleware:
- Identifies the client.
- Identifies the rate-limit policy for the requested endpoint.
- Generates a Redis key for the client and policy.
- Executes the Token Bucket logic through a Lua script.
- Allows the request if a token is available.
- Returns
429 Too Many Requestsif the bucket does not contain enough tokens.
For example:
/login -> 5 requests/minute
/search -> 30 requests/minute
/products -> 100 requests/minute
Each policy has its own bucket.
The Token Bucket algorithm models a bucket that holds a fixed number of tokens.
For example:
Capacity: 5 tokens
Refill rate: 1 token/second
Initially:
[5 tokens]
Each accepted request consumes one token:
Request 1 -> 4 tokens
Request 2 -> 3 tokens
Request 3 -> 2 tokens
Request 4 -> 1 token
Request 5 -> 0 tokens
Request 6 -> rejected
Tokens are continuously added according to the configured refill rate, up to the bucket capacity.
This allows short bursts of traffic while still controlling the average request rate.
Common rate-limiting algorithms include:
- Fixed Window
- Sliding Window
- Sliding Window Counter
- Token Bucket
- Leaky Bucket
Token Bucket was selected because it supports controlled bursts while maintaining a defined long-term request rate.
It is also relatively simple to implement and works well for API rate limiting.
Redis stores the state of each client's token bucket.
The state contains values such as:
tokens
last_refill
A Redis key follows this structure:
rate_limit:{client_id}:{policy}
For example:
rate_limit:127.0.0.1:login
rate_limit:127.0.0.1:products
rate_limit:127.0.0.1:search
This means the same client can have independent rate limits for different endpoints.
For example, exhausting the /login bucket does not affect the /products bucket.
A rate limiter needs shared state.
If the state is stored only in application memory, every server instance maintains a separate bucket:
Load Balancer
/ | \
/ | \
v v v
Server 1 Server 2 Server 3
| | |
Memory Memory Memory
This can result in inconsistent rate limits because each server has different state.
Redis provides a shared state store:
Load Balancer
/ | \
v v v
Server 1 Server 2 Server 3
\ | /
\ | /
Redis
All application instances can therefore access the same rate-limit state.
This is what allows the rate limiter to work across multiple application instances.
The Token Bucket operation involves multiple steps:
1. Read current tokens
2. Read last refill time
3. Calculate elapsed time
4. Refill tokens
5. Check available tokens
6. Consume a token
7. Store updated state
If these operations were performed as separate Redis requests, concurrent requests could read the same state before another request updates it.
The Lua script executes the complete operation inside Redis as a single atomic operation.
Request A ----\
Request B ----- > Redis Lua Script
Request C ----/ |
v
Atomic operation
This reduces race conditions when multiple requests access the same bucket concurrently.
The rate-limiting mechanism is separated from the policy configuration.
The mechanism defines how rate limiting works:
Token Bucket
Redis
Lua
The policy defines how much traffic is allowed:
LOGIN_POLICY
capacity = 5
refill_rate = 5/60
SEARCH_POLICY
capacity = 30
refill_rate = 30/60
PRODUCTS_POLICY
capacity = 100
refill_rate = 100/60
This separation makes it possible to change endpoint limits without changing the underlying rate-limiting algorithm.
Rate limiting is implemented as FastAPI middleware.
Without middleware, every endpoint would need to contain its own rate-limiting logic.
With middleware:
Request
|
v
Rate Limit Middleware
|
+---- Rejected -> 429
|
+---- Allowed
|
v
API Endpoint
This keeps the rate-limiting logic centralized and reusable.
When a client exceeds its configured limit, the middleware returns:
HTTP/1.1 429 Too Many RequestsExample response:
{
"detail": "Too many requests"
}Redis is currently running inside a Docker container while the FastAPI application runs locally.
Local Machine
|
+-- FastAPI
|
+-- Docker
|
+-- Redis
Start Redis with:
docker run --name rate-limiter-redis -p 6379:6379 -d redisCheck the Redis connection:
docker exec -it rate-limiter-redis redis-cli pingExpected output:
PONG
rate-limiter/
|
+-- app/
| +-- __init__.py
| +-- main.py
| +-- middleware.py
| +-- redis_client.py
| +-- redis_token_bucket.py
| +-- rate_limit_policy.py
|
+-- requirements.txt
+-- .gitignore
+-- README.md
Creates the FastAPI application and defines the API endpoints.
Intercepts incoming requests and performs the rate-limit check before the request reaches the endpoint.
Creates the Redis connection used by the application.
Contains the Redis-backed Token Bucket implementation and Lua script.
Contains the rate-limit policies for different API endpoints.
- Python 3.10+
- Docker
- Redis
- FastAPI
Install the Python dependencies:
pip install -r requirements.txtClone the repository:
git clone https://github.com/mohitrai810/distributed-rate-limiter
cd rate-limiterCreate and activate a virtual environment:
python -m venv venvWindows:
venv\Scripts\activateInstall dependencies:
pip install -r requirements.txtStart Redis:
docker run --name rate-limiter-redis -p 6379:6379 -d redisStart the FastAPI application:
uvicorn app.main:app --reloadThe API will be available at:
http://127.0.0.1:8000
The rate limiter can be tested by repeatedly sending requests to a protected endpoint.
For example:
GET /login
With a policy of 5 requests per minute:
200 OK
200 OK
200 OK
200 OK
200 OK
429 Too Many Requests
The /products endpoint uses a separate bucket, so exhausting the /login bucket does not affect it.
Redis state can also be inspected directly:
docker exec -it rate-limiter-redis redis-cliList rate-limit keys:
KEYS rate_limit:*
Inspect a bucket:
HGETALL rate_limit:127.0.0.1:login
This project demonstrates several system design concepts:
- Rate limiting
- Token Bucket algorithm
- Distributed shared state
- Redis data modeling
- Atomic operations
- Lua scripting
- Concurrency
- Middleware
- API design
- Separation of mechanism and policy
- Horizontal scaling considerations
The current version focuses on the core distributed rate-limiting implementation.
Planned improvements:
- Rate-limit response headers
Retry-Aftersupport- Async Redis
- Authentication-based rate limiting
- API-key-based rate limiting
- Configurable policies
- Load testing and benchmarking
- Prometheus metrics
- Monitoring and alerting
- Dockerize the FastAPI application
- Docker Compose for the complete stack
- Multiple FastAPI instances behind a load balancer
- Proxy-aware client identification.