Distributed rate limiting for FastAPI and Django with Redis backend. Built with love for scalable systems.
Project description
Pylimitx - Distributed Rate Limiting for Python
A distributed rate-limiting solution for FastAPI and Django applications.
uv add "pylimitx[fastapi]"
uv add "pylimitx[django]"
Features
- Multiple Algorithms: Sliding Window (accurate) and Token Bucket (burst-friendly)
- FastAPI Support: Middleware and decorators for FastAPI
- Django Support: Middleware and decorators for Django
- Distributed: Redis-backed, scales horizontally
- Reliable: Circuit breaker pattern, fail-open safety
- Simple: Easy to integrate, minimal configuration
- Fast: Uses Python and Lua scripts for atomic operations with minimal round-trips to Redis.
Installation
With FastAPI support:
Install via UV (recommended)
uv add "pylimitx[fastapi]"
If use pip
pip install "pylimitx[fastapi]"
With Django support:
Install via UV (recommended)
uv add "pylimitx[django]"
If use pip
pip install "pylimitx[django]"
IMPORTANT: Redis Setup (Required for Both FastAPI and Django)
Rate limiting requires a running Redis server. Set it up first:
1. Start Redis
# Using Docker (Recommended)
docker run -d -p 6379:6379 redis:latest
# Or install locally
brew install redis
redis-server
2. Verify Redis is Running
redis-cli ping
# Should return: PONG
3. Configure Redis URL in Your App
The default Redis URL is: redis://localhost:6379
If using a different URL, update it when initializing Pylimitx (see examples below).
FastAPI Usage
IMPORTANT: Middleware Must Be Added FIRST
Middleware MUST be added before route handlers are defined, so it checks requests BEFORE your backend logic runs. If a request exceeds the rate limit, it returns 429 immediately without executing your route.
Step 1: Initialize Redis and RateLimiter
from fastapi import FastAPI
from redis.asyncio import Redis
from pylimitx import RateLimiter, RateLimitMiddleware, rate_limit
app = FastAPI()
# Create Redis connection
redis = Redis.from_url("redis://localhost:6379", decode_responses=True)
# Create rate limiter instance (for decorators)
limiter = RateLimiter(redis=redis)
app.state.pylimitx_limiter = limiter
Step 2: Add Global Middleware (MUST BE FIRST in middleware stack)
# Add middleware BEFORE defining routes
# This ensures ALL requests are checked for rate limits FIRST
app.add_middleware(
RateLimitMiddleware,
redis_url="redis://localhost:6379",
limit=100, # 100 requests allowed
window=60, # per 60 seconds
)
# Now define your routes AFTER adding middleware
Step 3: Optional - Add Per-Route Rate Limiting
from pylimitx import rate_limit
@app.get("/api/search")
@rate_limit(limit=10, window=60) # Override global limit for this route
async def search(q: str):
return {"results": "..."}
Complete FastAPI Example
from fastapi import FastAPI, UploadFile
from redis.asyncio import Redis
from pylimitx import RateLimiter, RateLimitMiddleware, rate_limit
app = FastAPI()
# Step 1: Redis setup
redis = Redis.from_url("redis://localhost:6379", decode_responses=True)
app.state.pylimitx_limiter = RateLimiter(redis=redis)
# Step 2: Add middleware FIRST (before any routes)
app.add_middleware(
RateLimitMiddleware,
redis_url="redis://localhost:6379",
limit=100,
window=60,
)
# Step 3: NOW define your routes (they are protected by middleware)
@app.get("/api/search")
@rate_limit(limit=10, window=60) # Different limit for this endpoint
async def search(q: str):
return {"results": "..."}
@app.post("/api/upload")
async def upload(file: UploadFile):
# Uses global middleware limit (100/60)
return {"uploaded": True}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Django Usage
IMPORTANT: Middleware Must Be Placed Early in MIDDLEWARE List
Middleware checks requests BEFORE your views run. Place it HIGH in the MIDDLEWARE list so requests are checked FIRST. If a request exceeds the rate limit, it returns 429 immediately without calling your view.
Step 1: Configure Redis in settings.py (BEFORE middleware)
# settings.py
# Configure rate limiting
RATE_LIMIT_CONFIG = {
'REDIS_URL': 'redis://localhost:6379', # Must run Redis before using
'LIMIT': 100, # Default: 100 requests
'WINDOW': 60, # Default: per 60 seconds
}
Step 2: Option A - Use Decorators (Per-View Rate Limiting)
from django.http import HttpResponse
from pylimitx import django_rate_limit
@django_rate_limit(limit=20, window=60)
def search_view(request):
# Only allows 20 requests per 60 seconds to this view
return HttpResponse("Search results")
# Class-based view
from django.views import View
class ExportView(View):
@django_rate_limit(limit=5, window=3600)
def post(self, request):
# Only allows 5 requests per hour to this endpoint
return HttpResponse("Exporting...")
Step 2: Option B - Use Global Middleware (All Endpoints)
# settings.py
# Add middleware EARLY in the list (before other middleware that processes requests)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# Place rate limiting middleware EARLY, BEFORE processing middleware
'pylimitx.integrations.django.middleware.DjangoRateLimitMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
]
RATE_LIMIT_CONFIG = {
'REDIS_URL': 'redis://localhost:6379',
'LIMIT': 100,
'WINDOW': 60,
}
# Now ALL endpoints are rate limited to 100 requests per 60 seconds
# No changes needed in your views
Complete Django Example
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# Place rate limiting early - BEFORE processing middleware
'pylimitx.integrations.django.middleware.DjangoRateLimitMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
]
RATE_LIMIT_CONFIG = {
'REDIS_URL': 'redis://localhost:6379',
'LIMIT': 100,
'WINDOW': 60,
}
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('search/', views.search_view),
path('export/', views.ExportView.as_view()),
]
# views.py
from django.http import HttpResponse
from django.views import View
from pylimitx import django_rate_limit
# Per-view limit (overrides middleware limit)
@django_rate_limit(limit=20, window=60)
def search_view(request):
return HttpResponse("Search results")
# Class-based view with decorator
class ExportView(View):
@django_rate_limit(limit=5, window=3600)
def post(self, request):
return HttpResponse("Exporting...")
Rate Limiting Algorithms
Sliding Window (Default - Most Accurate)
Tracks exact request timestamps. Best for strict API limits.
# FastAPI
@rate_limit(limit=10, window=60, algorithm="sliding_window")
async def endpoint():
pass
# Django
RATE_LIMIT_CONFIG = {
'LIMIT': 10,
'WINDOW': 60,
'ALGORITHM': 'sliding_window', # Default
}
Token Bucket (Burst Support)
Allows users to burst requests up to bucket_capacity, then rate limits to the specified limit/window.
IMPORTANT: If you do NOT provide bucket_capacity and refill_rate:
- bucket_capacity defaults to limit value
- refill_rate defaults to limit/window (steady rate)
- Result: Works like sliding window with no burst allowed
# Example 1: With burst support
# FastAPI
@rate_limit(
limit=10,
window=60,
algorithm="token_bucket",
bucket_capacity=30, # Can burst up to 30 requests
refill_rate=10/60, # Then refill at 10 per minute
)
async def upload(file):
# Users can upload 30 files instantly, then limited to 10/min
pass
# Django
RATE_LIMIT_CONFIG = {
'LIMIT': 10,
'WINDOW': 60,
'ALGORITHM': 'token_bucket',
'BUCKET_CAPACITY': 30, # Burst size
'REFILL_RATE': 10/60, # Refill rate (requests per second)
}
# Example 2: Without burst (defaults used)
# FastAPI
@rate_limit(
limit=10,
window=60,
algorithm="token_bucket",
# bucket_capacity NOT provided -> defaults to 10
# refill_rate NOT provided -> defaults to 10/60 (0.167 per second)
)
async def endpoint():
# Same as sliding_window above - no burst, just rate limiting
pass
# Django
RATE_LIMIT_CONFIG = {
'LIMIT': 10,
'WINDOW': 60,
'ALGORITHM': 'token_bucket',
# BUCKET_CAPACITY NOT provided -> defaults to LIMIT (10)
# REFILL_RATE NOT provided -> defaults to LIMIT/WINDOW
# Result: Works like sliding_window with no burst
}
Understanding Parameters
- limit: Maximum requests allowed (e.g., 10)
- window: Time window in seconds (e.g., 60 for per minute)
- algorithm: Either "sliding_window" or "token_bucket"
- bucket_capacity (token_bucket only): Max burst size. If not provided, defaults to limit
- refill_rate (token_bucket only): How many requests refill per second. If not provided, defaults to limit/window
Response Headers
When a request is blocked (HTTP 429):
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
Retry-After: 45
When a request is allowed:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 3
Troubleshooting
Redis Connection Error
Error: "ConnectionError: Error 111 connecting to localhost:6379"
Solution:
# Make sure Redis is running
redis-cli ping
# Should return: PONG
# If not running, start Redis
redis-server # or docker run -d -p 6379:6379 redis:latest
Circuit Breaker Open
If you see many "pylimitx.redis_failure" messages, your Redis is down or disconnected.
What happens:
- Rate limiter fails to connect to Redis
- After 3 failures, circuit breaker opens
- All requests allowed (fail-open safety)
- Rate limiter retries connection after 30 seconds
Solution: Get Redis running again.
Configuration Reference
RATE_LIMIT_CONFIG (Django only)
RATE_LIMIT_CONFIG = {
'REDIS_URL': 'redis://localhost:6379', # Redis connection
'LIMIT': 100, # Max requests
'WINDOW': 60, # Time window (seconds)
'ALGORITHM': 'sliding_window', # Or 'token_bucket'
'BUCKET_CAPACITY': 200, # Token bucket only
'REFILL_RATE': 100/60, # Token bucket only
'FAIL_OPEN': True, # Allow traffic if Redis down
'FAILURE_THRESHOLD': 3, # Failures before circuit opens
'RECOVERY_TIMEOUT': 30, # Seconds before retry
}
Rate Limiter Options (FastAPI)
app.state.pylimitx_limiter = RateLimiter(
redis=redis,
fail_open=True, # Allow traffic if Redis down
failure_threshold=3, # Failures before circuit opens
recovery_timeout=30, # Seconds before retry
)
# Middleware options
app.add_middleware(
RateLimitMiddleware,
redis_url="redis://localhost:6379",
limit=100,
window=60,
algorithm="sliding_window",
bucket_capacity=None, # Token bucket only
refill_rate=None, # Token bucket only
fail_open=True,
failure_threshold=3,
recovery_timeout=30,
)
# Decorator options
@rate_limit(
limit=10,
window=60,
algorithm="sliding_window",
bucket_capacity=None,
refill_rate=None,
)
Key Concepts
Namespace: "global", "GET_/api/search" Identifier: IP address, API key, user ID Window: 60 (per minute), 3600 (per hour) Limit: Max requests in window
Contributing
Want to contribute? You can:
- Fork the repo: https://github.com/MANAS-CHARCHI/pylimit
- Use it for yourself and share feedback
- Contribute improvements via pull requests
- Report issues on GitHub
License
MIT License
Created with love for development and scalable systems 💙
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pylimitx-0.1.3.tar.gz.
File metadata
- Download URL: pylimitx-0.1.3.tar.gz
- Upload date:
- Size: 34.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
98df611a96afb6c9ecc21a2c49efe1037e936ca0b8e3719e12f288c700709b60
|
|
| MD5 |
3994f279a59792d608901db9e4f56b08
|
|
| BLAKE2b-256 |
8135fcbc0eb961b979edffbbe6b2458291b5a2a7bae00688930abef92d25b8d7
|
File details
Details for the file pylimitx-0.1.3-py3-none-any.whl.
File metadata
- Download URL: pylimitx-0.1.3-py3-none-any.whl
- Upload date:
- Size: 17.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bcabb539c5c6fab28bae2f7e396051e8c8fddeb59a05ef57346474e389c5e494
|
|
| MD5 |
328c18990ac78b69572d9d4cb2b69a8a
|
|
| BLAKE2b-256 |
8af7c33270d67c13da39ba9c9ab5d4b52220ccc5b1901acf20cb6e17f65b1f41
|