Skip to content

Rate Limiting

Rate limiting protects authentication endpoints from brute-force attacks and abuse. AuthFort uses a sliding window counter — no external dependencies required.

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.

EndpointDefaultLimit Type
login5/minIP + email
signup3/minIP + email
magic_link5/minIP + email
otp5/minIP + email
verify_email5/minIP only
refresh30/minIP only
oauth_authorize10/minIP only

Format: "{count}/{period}" — period can be sec, min, hour, or day.

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
    ),
)

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-After header with seconds until the limit resets
@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:

FieldTypeDescription
endpointstrWhich endpoint was hit
ip_addressstr | NoneClient IP
emailstr | NoneEmail (for email-based limits)
limitstrThe limit that was exceeded
key_typestr"ip" or "email"

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.

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.