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 | Config |
|---|---|---|
| Login | 5/min | AUTH_RATE_LIMIT_LOGIN |
| Register | 3/min | AUTH_RATE_LIMIT_REGISTER |
| Password reset / email verify | 3/min | AUTH_RATE_LIMIT_PASSWORD_RESET |
| Passkey authenticate | 10/min | AUTH_RATE_LIMIT_PASSKEY_AUTH |
| Refresh | 30/min | AUTH_RATE_LIMIT_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.
config = FullAuthConfig(
SECRET_KEY="...",
AUTH_RATE_LIMIT_LOGIN=10, # 10 login attempts per window
AUTH_RATE_LIMIT_REGISTER=5, # 5 registrations per window
AUTH_RATE_LIMIT_WINDOW_SECONDS=120, # 2-minute window
)
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,
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,
)
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:
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 (e.g. 1.2.3.4, 10.0.0.1), the first IP (original client) is used.
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.