A high-performance, Redis-backed rate limiter for FastAPI applications with dynamic rate limits.
Project description
Flexible Rate Limiter
A high-performance, Redis-backed rate limiter for FastAPI applications.
Unlike standard rate limiters that apply static limits per endpoint, Flexible Rate Limiter allows you to enforce limits based on user-specific plans injected into the request state. It supports atomic operations via Lua scripts, ensuring data integrity even under high concurrency.
Features
- ⚡ High Performance: Uses Lua scripting to perform check-and-decrement operations atomically in a single network round-trip.
- 🧠 Context Aware: Reads limits dynamically from
request.state, enabling tiered limits (e.g., Free vs. Enterprise). - 🛡️ Fail-Open Design: If Redis becomes unreachable, the limiter automatically allows requests to proceed, preventing your API from crashing (Reliability > Strictness).
- 🌍 Scopes: Support for both Endpoint-specific limits (Local) and Global API limits.
- ⚖️ Weighted Costs: Assign different costs to expensive endpoints statically or dynamically.
- ⏱️ Flexible Windows: Supports natural language durations (e.g., "1 hour", "2 days").
- 🗣️ Humanized Errors: Returns "Try again in 5 minutes" style messages.
Installation
pip install flexible-rate-limiter
Note: Requires a running Redis instance.
🚀 Best Used With
While this package can function standalone, it is designed to work seamlessly with subs-webhook.
subs-webhook handles the complexity of API keys, plan management, and webhooks (e.g., Pabbly), automatically populating the request.state.rate_limit configuration that this package consumes.
Integration Example
from fastapi import FastAPI, Depends
from subs_webhook import init_subs, validate_access
from flexible_rate_limiter import RateLimiter
app = FastAPI()
# 1. Initialize Subscription System (Handles Auth, Plans & State Injection)
init_subs(app, sqlite_path="./subs.db", redis_url="redis://localhost:6379/0")
# 2. Initialize Rate Limiter
limiter = RateLimiter(redis_url="redis://localhost:6379/0")
# Load your plan permissions (JSON config)
plans_config = {
"cost": 1,
"limit": 2000,
"window": "1 day",
# Rate specific for route
"/api/v1/rates/analytics": {
"cost": 5,
"limit": 200,
"window": "1 day"
}
}
# 3. Protect Route
@app.get("/api/analytics", dependencies=[
# validate_access checks permissions and injects request.state.rate_limit
Depends(validate_access(plans_config)),
# limiter reads that state and enforces the limit
Depends(limiter)
])
async def get_analytics():
return {"data": "..."}
Prerequisites (Standalone Usage)
If you are not using subs-webhook, your authentication middleware must manually populate:
request.state.api_key: A unique identifier for the user.request.state.rate_limit: A dictionary containing the rate limit configuration.
Usage
Basic Setup
Initialize the RateLimiter and add it as a dependency.
from fastapi import FastAPI, Request, Depends
from flexible_rate_limiter import RateLimiter
app = FastAPI()
# Initialize the limiter (Default cost = 1)
limiter = RateLimiter(redis_url="redis://localhost:6379/0")
# Mock Middleware (Use this if NOT using subs-webhook)
@app.middleware("http")
async def attach_user_plan(request: Request, call_next):
# In a real app, this is fetched from DB/Redis based on the API Key
request.state.api_key = "user_123"
# Inject Rate Limit Configuration
request.state.rate_limit = {
"cost": 1,
"limit": 5000,
"window": "1 hour"
}
response = await call_next(request)
return response
# Apply to Routes
@app.get("/api/data", dependencies=[Depends(limiter)])
async def get_data():
return {"message": "Request successful"}
Configuration Structure
The request.state.rate_limit dictionary is flexible. It allows you to define a Global Default and specific Route Overrides.
1. Simple Global Limit
This applies the same limit (5000 requests per hour) to every endpoint the user accesses.
{
"cost": 1,
"limit": 5000,
"window": "1 hour"
}
2. Global Limit + Route Override
In this scenario, the user has a global bucket of 2000 requests per day. However, accessing the /analytics endpoint is restricted to a separate, smaller bucket (200 requests/day).
Note: Nested keys starting with / are treated as route-specific overrides (Local Scope).
{
"cost": 1, // Default cost per request
"limit": 2000, // Global Limit
"window": "1 day", // Global Window
"/api/v1/rates/analytics": { // Route-Specific Override (Local Scope)
"cost": 5, // Expensive endpoint costs 5 units
"limit": 200, // Strict limit for this path
"window": "1 day"
}
}
3. Logic & Precedence
The RateLimiter determines which rule to apply based on the current request path:
- Check for Override: Does the config dictionary contain a key matching
request.url.path?- Yes: Use the settings inside that nested dictionary. The scope is Local (only hits to this specific path count against this limit).
- Fallback to Default: If no override exists, does the root dictionary contain
limit?- Yes: Use the root settings. The scope is Global (hits to any non-overridden path share this single counter).
- No Config: If neither exists, rate limiting is skipped.
HTTP Responses
Success (200 OK)
Headers indicate the limit that was applied (Global or Route-specific):
HTTP/1.1 200 OK
X-RateLimit-Limit: 2000
X-RateLimit-Remaining: 1999
X-RateLimit-Window: 1 day
Limit Exceeded (429 Too Many Requests)
{
"detail": "Rate limit exceeded. Try again in 15 minutes."
}
Reliability
This package implements a Fail-Open strategy. If there is an error such as the Redis connection failing or timing out, the RateLimiter catches the error, logs it, and allows the request to proceed. This ensures your API service remains available even if the caching layer is down.
Logging
To view error logs (e.g., Redis failures), configure the logger in your application startup:
import logging
# Configure logging to see errors from the rate limiter
logging.basicConfig(level=logging.INFO)
# OR configure specifically for this library
logger = logging.getLogger("flexible_rate_limiter")
logger.setLevel(logging.ERROR)
License
This software is released under the MIT License. Copyright (c) 2026 Anthony Mugendi.
Project details
Release history Release notifications | RSS feed
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 flexible_rate_limiter-0.1.0.tar.gz.
File metadata
- Download URL: flexible_rate_limiter-0.1.0.tar.gz
- Upload date:
- Size: 16.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.4.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
525be50fa1d1d89e191c3ab5a6d8285ca7221cfcd35086b408435f48b0da7c5b
|
|
| MD5 |
707a5e3aab4509d1fd8048ae56ae9956
|
|
| BLAKE2b-256 |
ffac4579c4d897169a65425c288bdce5367937fbeede6f82073d60315a444131
|
File details
Details for the file flexible_rate_limiter-0.1.0-py3-none-any.whl.
File metadata
- Download URL: flexible_rate_limiter-0.1.0-py3-none-any.whl
- Upload date:
- Size: 8.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.4.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
569bb844e58d3e2b3cb1f77931c10d831b3d0809f94c93560d4059ff5074e3d2
|
|
| MD5 |
ec31c80c6c88540fd1a23a6f5aae6ce9
|
|
| BLAKE2b-256 |
ca7a3ddb9a7410f6d954f8aba9f5500def5989e4352d1b2ff44b79de86b72b9d
|