Rate Limiting¶
fastapi-fullauth provides two levels of rate limiting, plus account lockout for brute-force protection:
- Auth rate limits: per-route limits on login, register, password reset, passkey-authenticate, and refresh. Built into the routers, enabled by default.
RateLimitMiddleware: a generic per-IP request limiter. Not wired automatically; add it yourself withapp.add_middleware(...).- Account lockout: locks accounts after too many failed login attempts. Different from rate limiting: lockout is per-account (by email), rate limiting is per-IP.
Auth rate limits¶
Enabled by default. Protects auth endpoints from brute force:
| Route | Default limit | AUTH_RATE_LIMITS field |
|---|---|---|
| Login | 5/min | login |
| Register | 3/min | register |
| Password reset / email verify | 3/min | password_reset |
| Passkey authenticate | 10/min | passkey_auth |
| Refresh | 30/min | refresh |
All routes share the same window (AUTH_RATE_LIMIT_WINDOW_SECONDS, default 60). The algorithm is sliding-window: it tracks request timestamps and counts how many fall within the current window.
The per-route caps are grouped under AUTH_RATE_LIMITS. Pass an AuthRateLimits and only set the routes you want to change; the rest keep their defaults:
from fastapi_fullauth import AuthRateLimits, FullAuthConfig
config = FullAuthConfig(
SECRET_KEY="...",
AUTH_RATE_LIMITS=AuthRateLimits(login=10, register=5),
AUTH_RATE_LIMIT_WINDOW_SECONDS=120, # 2-minute window
)
From the environment, set it as a JSON object: FULLAUTH_AUTH_RATE_LIMITS='{"login": 10, "register": 5}'.
When a limit is exceeded, the route returns 429 Too Many Requests with a Retry-After header.
Disable entirely:
How auth rate limits differ from lockout¶
Auth rate limits are per-IP: they prevent a single IP from hammering auth endpoints. Account lockout is per-account: it locks a specific email after too many failed logins, regardless of which IP the attempts came from.
Both can trigger at the same time. A single attacker hitting one account will see rate limiting first (IP-based, 5 attempts), then lockout kicks in (account-based, 5 failures).
Account lockout¶
Locks an account after repeated failed login attempts. Enabled by default.
| Setting | Default | Description |
|---|---|---|
LOCKOUT_ENABLED |
True |
Enable/disable lockout |
LOCKOUT_BACKEND |
"memory" |
"memory" or "redis" |
MAX_LOGIN_ATTEMPTS |
5 |
Failed attempts before lockout |
LOCKOUT_DURATION_MINUTES |
15 |
How long the account stays locked |
When an account is locked, login attempts return 423 Locked with a message indicating the lockout duration.
Lockout is checked at the start of every login attempt, before any password verification. A successful login clears the failure counter.
Global rate limit middleware¶
RateLimitMiddleware limits every request to the app per client IP. Useful when you don't already have a CDN/WAF in front of the service.
from fastapi_fullauth.middleware import RateLimitMiddleware
app.add_middleware(
RateLimitMiddleware,
max_requests=60,
window_seconds=60,
trusted_proxy_headers=fullauth.config.TRUSTED_PROXY_HEADERS,
trusted_proxy_count=fullauth.config.TRUSTED_PROXY_COUNT,
exempt_paths=["/health", "/metrics"],
)
Default: 60 requests per 60 seconds per IP.
Every response includes rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per window |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Seconds until the window resets |
When the limit is exceeded, a 429 Too Many Requests response is returned.
Redis-backed global limit¶
Build the limiter with create_rate_limiter() so it uses the same backend as the rest of the library:
from fastapi_fullauth.middleware import RateLimitMiddleware
from fastapi_fullauth.protection import create_rate_limiter
limiter = create_rate_limiter(fullauth.config, max_requests=60, window_seconds=60)
app.add_middleware(
RateLimitMiddleware,
limiter=limiter,
trusted_proxy_headers=fullauth.config.TRUSTED_PROXY_HEADERS,
trusted_proxy_count=fullauth.config.TRUSTED_PROXY_COUNT,
)
Set RATE_LIMIT_BACKEND="redis" and REDIS_URL=... on the config to switch to Redis. The in-memory backend is per-process; fine for a single worker, broken on multi-worker / multi-pod.
Proxy support¶
Behind a reverse proxy (Nginx, Cloudflare, AWS ALB), request.client.host is the proxy's IP, not the real user's. Configure trusted proxy headers so rate limiting and lockout work correctly:
config = FullAuthConfig(
SECRET_KEY="...",
TRUSTED_PROXY_HEADERS=["X-Forwarded-For"],
TRUSTED_PROXY_COUNT=1, # number of trusted proxies in front of the app
)
Warning
Only list headers you trust. If your server is directly exposed to the internet (no proxy), leave this empty; otherwise users can spoof their IP via the header.
When X-Forwarded-For contains a chain, the client IP is read from the right, not the left. Each trusted proxy appends the address it received from, so the right-most entries are set by your infrastructure while the left-most are supplied by the client and are spoofable. TRUSTED_PROXY_COUNT (default 1) is how many proxy hops sit in front of the app; the resolver returns the entry that many positions from the right. For a chain 6.6.6.6, 203.0.113.9 behind one proxy, 203.0.113.9 is used and the client-injected 6.6.6.6 is ignored.
Danger
Set TRUSTED_PROXY_COUNT to your real proxy depth. If it is larger than the number of proxies that actually append the header, an attacker can pad the chain and control the resolved IP, defeating rate limiting and lockout. When the chain is shorter than the configured count, the direct peer is used instead of a client-supplied value.
Common proxy configurations:
| Proxy | Header to trust |
|---|---|
Nginx (proxy_set_header X-Forwarded-For) |
X-Forwarded-For |
| Cloudflare | CF-Connecting-IP |
| AWS ALB | X-Forwarded-For |
| Google Cloud Load Balancer | X-Forwarded-For |
This setting applies to both auth rate limits and any RateLimitMiddleware you wire up.
Custom backends¶
Register your own lockout or rate limiter backend:
from fastapi_fullauth.protection.lockout import LockoutManager, register_lockout_backend
from fastapi_fullauth.protection.ratelimit import register_rate_limiter_backend
class DatabaseLockoutManager(LockoutManager):
def __init__(self, max_attempts, lockout_seconds, **kwargs):
super().__init__(max_attempts, lockout_seconds)
async def is_locked(self, key: str) -> bool: ...
async def record_failure(self, key: str) -> None: ...
async def clear(self, key: str) -> None: ...
register_lockout_backend("database", DatabaseLockoutManager)
# Then set LOCKOUT_BACKEND="database" in config
The same pattern works for rate limiters with register_rate_limiter_backend().
Production recommendations¶
- Use Redis backends for all protection subsystems (
FULLAUTH_BACKEND=redis). In-memory backends are per-process and don't work correctly with multiple workers. - Layer auth rate limits with global middleware: auth rate limits protect specific routes (login, register), while
RateLimitMiddlewareprotects the entire app from IP-level abuse. - If you have a CDN or WAF (Cloudflare, AWS WAF), DDoS protection happens at that layer. The library's rate limiting handles application-level abuse (credential stuffing, enumeration).
- Configure TRUSTED_PROXY_HEADERS correctly. Without it, all requests appear to come from the proxy's IP, and rate limiting is effectively disabled.