Rate Limiting
Rate limiting protects authentication endpoints from brute-force attacks and abuse. AuthFort uses a sliding window counter — no external dependencies required.
Enable Rate Limiting
Section titled “Enable Rate Limiting”Pass RateLimitConfig to the constructor. All endpoints get sensible defaults.
from authfort import AuthFort, RateLimitConfig
auth = AuthFort(
database_url="postgresql+asyncpg://...",
rate_limit=RateLimitConfig(),
) from authfort import AuthFort, RateLimitConfig
auth = AuthFort(
database_url="postgresql+asyncpg://...",
rate_limit=RateLimitConfig(),
) That’s it. All 8 auth endpoints are now rate limited.
Default Limits
Section titled “Default Limits”| Endpoint | Default | Limit Type |
|---|---|---|
login | 5/min | IP + email |
signup | 3/min | IP + email |
magic_link | 5/min | IP + email |
otp | 5/min | IP + email |
verify_email | 5/min | IP only |
refresh | 30/min | IP only |
oauth_authorize | 10/min | IP only |
Format: "{count}/{period}" — period can be sec, min, hour, or day.
Custom Limits
Section titled “Custom Limits”Override specific endpoints or disable them with None.
auth = AuthFort(
database_url="postgresql+asyncpg://...",
rate_limit=RateLimitConfig(
login="10/min",
signup="5/min",
refresh=None, # disable for refresh
),
) auth = AuthFort(
database_url="postgresql+asyncpg://...",
rate_limit=RateLimitConfig(
login="10/min",
signup="5/min",
refresh=None, # disable for refresh
),
) How It Works
Section titled “How It Works”Rate limiting uses two strategies:
- IP-based — applied to all endpoints. Prevents a single IP from flooding.
- Email-based — applied to login, signup, magic link, and OTP. Catches distributed attacks targeting the same account from multiple IPs.
Both must pass for the request to proceed.
When a limit is exceeded, AuthFort returns:
- 429 Too Many Requests status code
Retry-Afterheader with seconds until the limit resets
Events
Section titled “Events”@auth.on("rate_limit_exceeded")
async def on_rate_limit(event):
print(f"{event.key_type} limit hit on {event.endpoint}: {event.ip_address}") @auth.on("rate_limit_exceeded")
async def on_rate_limit(event):
print(f"{event.key_type} limit hit on {event.endpoint}: {event.ip_address}") The rate_limit_exceeded event fires on every rejected request with:
| Field | Type | Description |
|---|---|---|
endpoint | str | Which endpoint was hit |
ip_address | str | None | Client IP |
email | str | None | Email (for email-based limits) |
limit | str | The limit that was exceeded |
key_type | str | "ip" or "email" |
Behind a Reverse Proxy
Section titled “Behind a Reverse Proxy”When AuthFort runs behind a reverse proxy (nginx, traefik, Docker), request.client.host returns the proxy’s internal IP. Without proxy configuration, all users share a single rate limit bucket.
Option 1: Trust all proxies (simple, for single-proxy setups)
auth = AuthFort( database_url="...", trust_proxy=True, rate_limit=RateLimitConfig(),)Option 2: Trust specific proxies (recommended for production)
auth = AuthFort( database_url="...", trusted_proxies=["172.18.0.0/16"], rate_limit=RateLimitConfig(),)AuthFort reads X-Forwarded-For (leftmost value) then falls back to X-Real-IP. If neither header is present, request.client.host is used.
See Configuration for details on trust_proxy and trusted_proxies.
Custom Storage Backend
Section titled “Custom Storage Backend”The default InMemoryStore works for single-process deployments. For multi-process or multi-server setups, implement the RateLimitStore protocol with a shared backend like Redis.
from authfort.ratelimit import RateLimitStore
class RedisRateLimitStore: def hit(self, key, limit): # Returns: (allowed: bool, remaining: int, retry_after: float) ...
def reset(self, key=None): ...See Server Config for the full RateLimitStore protocol.