Async rate limiter for FastAPI with Redis or in-memory backend and advanced proxy-aware security
Project description
fastapi‑easylimiter
An ASGI async rate-limiting middleware for FastAPI with Redis or in-memory caching, designed to handle auto-generated routes (e.g., FastAPI-Users) without decorators, for simplicity and performance.
Features
- Async rate limiting
- Optional temporary IP bans
- Configurable threshold (default 10 violations)
- Sliding 15-min offense window
- 5-min ban on repeat abuse
- Cache Backends
- Redis (recommended for multi-instance deployments)
- In-Memory (single worker dev)
- Path-based rules
- Supports multi-rule prefix matching
- Global and per-route limits
- Standard rate-limit headers
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-Afteron429responses
- Proxy Aware
- Uses
'X-Forwarded-For'only when the sender is trusted, hardened logic - Rejects spoofed XFF headers
- Supports
'CF-Connecting-IP'from Cloudflare, verified against CF CIDRs, when enabled- Must be public not private IP range.
- Fallback to ASGI
scope["client"]if no trusted headers exist
- Uses
- Zero dependencies beyond Redis client
- Starlette-style ASGI middleware
- Custom responses
HTMLResponsefor browser clientsJSONResponsefor API clients
- CRLF-injection–safe header parsing
- Allows banning direct connections (no proxy/CF) for dev/testing
Installation
pip install fastapi-easylimiter
Usage
from fastapi import FastAPI
from fastapi_easylimiter import AsyncRedisBackend, InMemoryBackend, RateLimiterMiddleware
import redis.asyncio as redis_async
app = FastAPI()
REDIS_URL = "redis://localhost:6379/0"
# Redis backend (recommended for multi-instance deployments)
redis_client = redis_async.from_url(REDIS_URL, decode_responses=True)
backend = AsyncRedisBackend(
redis_client, # Redis pool/client
fail_open=False, # Allow requests if Redis is down
key_prefix="ratelimit:", # Redis key prefix
eval_timeout=30 # Eval timeout
)
# Or for single-instance/local development:
# backend = InMemoryBackend()
rules = {
"/": {"limit": 50, "period": 5}, # GLOBAL: 50 req/5sec per IP
"/api/": {"limit": 10, "period": 1}, # API: 10 req/sec per IP
"/api/users": {"limit": 30, "period": 60}, # USER ROUTES: 30 req/60sec per IP
}
app.add_middleware(
RateLimiterMiddleware,
rules=rules,
backend=backend,
trusted_proxies=None, # OPTIONAL: your proxy IPs (Only set if behind a proxy such as nginx)
cloudflare=False, # OPTIONAL: enable CF-Connecting-IP (Only set when behind cloudflare)
enable_bans=True, # OPTIONAL: enable temporary bans
ban_threshold=10, # Violations before ban
ban_duration=300, # Ban length in seconds
offense_ttl=600, # Offense counting window
ban_page="<p>Your IP has been temporarily banned.</p>", # OPTIONAL custom HTML ban page
rate_page="<p>Too many requests. Please try again later.</p>", # OPTIONAL custom HTML rate-limit page
ban_direct=True # ONLY SET WHEN RUNNING BAREMETAL NO CF AND NO PROXY
)
Example:
/api/users/mematches/api/usersand/api. If any rule is exceeded →429returned.
Redis Lua Script
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local count = redis.call("INCR", key)
local ttl = redis.call("TTL", key)
if ttl == -1 then
redis.call("EXPIRE", key, window)
ttl = window
end
local reset = now + ttl
local exceeded = count > limit and 1 or 0
return {count, reset, exceeded}
Redis Key Patterns
| Full Key Pattern | Example | Purpose |
|---|---|---|
ratelimit:rl:{client_ip}:{prefix} |
ratelimit:rl:203.0.113.5:/api |
Rate-limit counter per IP + route prefix |
ratelimit:ban:{client_ip} |
ratelimit:ban:203.0.113.5 |
Temporary ban flag |
ratelimit:offenses:{client_ip} |
ratelimit:offenses:203.0.113.5 |
Offense counter used for tracking |
Middleware Parameters
| Parameter | Type | Description |
|---|---|---|
app |
ASGIApp | FastAPI/ASGI app |
rules |
dict | { prefix: {"limit": int, "period": int} } |
backend |
Redis or InMemory backend | Rate-limit storage |
trusted_proxies |
list[str] | Proxies allowed to trust XFF headers |
cloudflare |
bool | Enable Cloudflare IP extraction |
enable_bans |
bool | Enable temporary IP bans |
ban_threshold |
int | Violations before ban |
ban_duration |
int | Ban length in seconds |
offense_ttl |
int | Offense counting window in seconds |
ban_page |
str | Custom HTML ban page |
rate_page |
str | Custom HTML rate-limit page |
ban_direct |
bool | Bypass all checks and ban directly |
Screenshot
Contributing
Contributions and forks are always welcome! Adapt, improve, or extend for your own needs.
Support
Parts of this code were generated/assisted by AI (Claude, Grok).
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 fastapi_easylimiter-0.3.7.tar.gz.
File metadata
- Download URL: fastapi_easylimiter-0.3.7.tar.gz
- Upload date:
- Size: 13.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a00ebbfb86a982d41b58eb5e9fbc16988afc6e659982585bcfdbc0c65e35732
|
|
| MD5 |
addfe343a70699dba63c4a590494036b
|
|
| BLAKE2b-256 |
e713a4ae75216a8f6bdbbddc530772a6b52bca3a346d12146d503a73ffc98489
|
File details
Details for the file fastapi_easylimiter-0.3.7-py3-none-any.whl.
File metadata
- Download URL: fastapi_easylimiter-0.3.7-py3-none-any.whl
- Upload date:
- Size: 12.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94454c0e7c524b6f22e941a816028490f6334784cf99096d6b313d82fa7e90bb
|
|
| MD5 |
e69ed08e7a740b2a65c55d78aab8437d
|
|
| BLAKE2b-256 |
fd10f2ba0f19d574f44e2c51a13e1a2e1a6f9fdc0ff5e4e05a618daaca74a094
|