Skip to content

Rate limiting

Optional per-endpoint limits protect login, registration, token flows, and TOTP surfaces from brute force and abuse. Configure AuthRateLimitConfig on LitestarAuthConfig.rate_limit_config. For the current Redis-backed contract, including the stable slot and group inventory, override patterns, and the paired TOTP Redis-store wiring, use Configuration as the maintained source of truth. This guide focuses on how the rate-limit surface maps onto the HTTP routes you expose.

Start with a preset

Most applications should start with one of the built-in AuthRateLimitConfig factories instead of building every slot manually.

AuthRateLimitConfig.strict()

Use strict() when the public sign-in surface should share one intentionally low-budget backend. This preset wires that backend to login, register, and totp_verify with the package default scopes and route-style namespaces. This in-memory example is for single-process development or pytest-style tests:

from litestar_auth.ratelimit import AuthRateLimitConfig, InMemoryRateLimiter

rate_limit_config = AuthRateLimitConfig.strict(
    backend=InMemoryRateLimiter(max_attempts=5, window_seconds=60),
)

AuthRateLimitConfig.lenient()

Use lenient() when login, refresh, and register can share a broader built-in budget, but password-reset, verification, and TOTP routes still need stricter protection. The preset preserves the backend you pass for the lower-risk slots and clones it with a five-attempt cap for the sensitive routes. This in-memory example is for single-process development or tests:

from litestar_auth.ratelimit import AuthRateLimitConfig, InMemoryRateLimiter

rate_limit_config = AuthRateLimitConfig.lenient(
    backend=InMemoryRateLimiter(max_attempts=20, window_seconds=300),
)

AuthRateLimitConfig.disabled()

Use disabled() for local development, tests, or other trusted environments that want the plugin surface wired without active throttling. The preset returns an AuthRateLimitConfig with every slot left unset.

from litestar_auth.ratelimit import AuthRateLimitConfig

rate_limit_config = AuthRateLimitConfig.disabled()

Custom shared-backend setup

Use Configuration for the maintained production Redis/TOTP recipe. That shared-client snippet is the single source of truth for wiring RedisAuthPreset, AuthRateLimitSlot, TotpConfig.totp_enrollment_store, TotpConfig.totp_pending_jti_store, and TotpConfig.totp_used_tokens_store from one shared async Redis client.

This guide deliberately does not repeat the full snippet, because the slot inventory, namespace families, and shared-client TOTP wiring are documented in one maintained place. Keep AuthRateLimitConfig.from_shared_backend() plus direct RedisRateLimiter(...) / RedisTotpEnrollmentStore(...) / RedisJWTDenylistStore(...) / RedisUsedTotpCodeStore(...) construction as the advanced low-level path when applications intentionally need separate backends or deeper per-slot customization.

Reach for from_shared_backend() when the preset is close, but you still need endpoint- or group-specific changes. Prefer endpoint_overrides for per-slot customization. The in-memory backend shown here is only for single-process development or tests; use RedisRateLimiter or RedisAuthPreset for multi-worker production.

from litestar_auth.ratelimit import (
    AuthRateLimitConfig,
    AuthRateLimitSlot,
    EndpointRateLimit,
    InMemoryRateLimiter,
    SharedRateLimitConfigOptions,
)

shared_backend = InMemoryRateLimiter(max_attempts=10, window_seconds=60)

rate_limit_config = AuthRateLimitConfig.from_shared_backend(
    shared_backend,
    options=SharedRateLimitConfigOptions(
        trusted_proxy=True,
        trusted_proxy_hops=2,
        endpoint_overrides={
            AuthRateLimitSlot.TOTP_VERIFY: EndpointRateLimit(
                backend=shared_backend,
                scope="ip",
                namespace="totp-verify",
            ),
        },
    ),
)

enabled and disabled live on SharedRateLimitConfigOptions. When app code needs the supported slot inventory directly, iterate AuthRateLimitSlot. Use tuple(AuthRateLimitSlot) for explicit enabled=... options, and use {AuthRateLimitSlot.VERIFY_TOKEN, AuthRateLimitSlot.REQUEST_VERIFY_TOKEN} for disabled=... options when the built-in verification routes stay off.

When rate-limited traffic reaches the app through trusted reverse proxies, set trusted_proxy=True only after your edge strips or overwrites client-supplied forwarding headers. The default trusted_proxy_hops=1 keeps the existing single-proxy behavior and selects the rightmost X-Forwarded-For entry. Multi-proxy chains can set trusted_proxy_hops=N to select the Nth entry from the right; if the header has fewer entries, the limiter fails closed to request.client.host.

AuthRateLimitSlot

Use the AuthRateLimitSlot enum when application code, annotations, or override mappings refer to supported auth rate-limit slots. The enum is the IDE-friendly public inventory for enabled, disabled, endpoint_overrides, and other slot-keyed inputs.

from litestar_auth.ratelimit import AuthRateLimitSlot

AuthRateLimitSlot.LOGIN
AuthRateLimitSlot.REQUEST_VERIFY_TOKEN
AuthRateLimitSlot.TOTP_VERIFY

Behavior

  • When a limit is exceeded, clients receive 429 Too Many Requests with Retry-After.
  • Controller assembly emits SecurityWarning when public unauthenticated endpoints are exposed without their matching rate-limit slots: login for POST .../login, refresh when refresh-token rotation is enabled, and register for POST .../register. Configure the matching AuthRateLimitConfig fields for production. unsafe_testing=True suppresses this warning for controlled tests and local-only scaffolding.
  • Backends: InMemoryRateLimiter (single process / dev) or RedisRateLimiter (production, multiple workers). See Deployment.
  • InMemoryRateLimiter fails closed for new keys when max_keys is reached and no expired counters can be pruned. It logs event=rate_limit_memory_capacity; size max_keys for local/dev traffic or use Redis for public multi-worker deployments.
  • LitestarAuthConfig.deployment_worker_count is the explicit topology declaration for startup validation. None means unknown worker count and preserves warning-only diagnostics, 1 means known single-worker, and values greater than 1 fail closed if any enabled auth rate-limit slot uses a process-local backend.
  • Backend keys use scoped PBKDF2-HMAC-SHA-256 digests for IP addresses, normalized identifiers, and API-key ids so raw request identifiers are not written into Redis or process-local key maps. Changing this digest contract invalidates existing active rate-limit windows after upgrade; it does not affect persisted users, tokens, or API keys. These digests protect backend key shape and avoid raw identifier storage; they are not a privacy boundary against brute-force guessing of low-entropy identifiers such as common IPs.
  • For the production shared-client Redis path, use the configuration recipe so rate limiting stays aligned with the TOTP Redis stores instead of hand-maintaining a partial copy in this guide.
  • For pytest-driven plugin tests, InMemoryRateLimiter is the documented single-process choice described in the testing guide. Keep limiter state isolated per test when counters must not leak.

Config fields → HTTP surface

Each field accepts an EndpointRateLimit (or None to disable that bucket). Map them to routes you expose:

AuthRateLimitSlot is the public slot inventory. Iterate it directly for ordered enabled=... inputs, and pass {AuthRateLimitSlot.VERIFY_TOKEN, AuthRateLimitSlot.REQUEST_VERIFY_TOKEN} when verification endpoints should stay disabled.

AuthRateLimitConfig field / AuthRateLimitSlot value AuthRateLimitEndpointGroup value Default scope Default namespace token Typical route / action
login / AuthRateLimitSlot.LOGIN login ip_email login POST {auth}/login
refresh / AuthRateLimitSlot.REFRESH refresh ip refresh POST {auth}/refresh
register / AuthRateLimitSlot.REGISTER register ip register POST {auth}/register
forgot_password / AuthRateLimitSlot.FORGOT_PASSWORD password_reset ip_email forgot-password POST {auth}/forgot-password
reset_password / AuthRateLimitSlot.RESET_PASSWORD password_reset ip reset-password POST {auth}/reset-password
change_password / AuthRateLimitSlot.CHANGE_PASSWORD login ip_email change-password POST {users}/me/change-password
verify_token / AuthRateLimitSlot.VERIFY_TOKEN verification ip verify-token POST {auth}/verify
request_verify_token / AuthRateLimitSlot.REQUEST_VERIFY_TOKEN verification ip_email request-verify-token POST {auth}/request-verify-token
organization_switch / AuthRateLimitSlot.ORGANIZATION_SWITCH organizations ip organization-switch POST {auth}/switch-organization
organization_invitation_accept / AuthRateLimitSlot.ORGANIZATION_INVITATION_ACCEPT organization_invitations ip organization-invitation-accept POST {auth}/organization-invitations/accept
organization_invitation_decline / AuthRateLimitSlot.ORGANIZATION_INVITATION_DECLINE organization_invitations ip organization-invitation-decline POST {auth}/organization-invitations/decline
totp_enable / AuthRateLimitSlot.TOTP_ENABLE totp ip totp-enable POST {auth}/2fa/enable
totp_confirm_enable / AuthRateLimitSlot.TOTP_CONFIRM_ENABLE totp ip totp-confirm-enable POST {auth}/2fa/enable/confirm
totp_verify / AuthRateLimitSlot.TOTP_VERIFY totp ip totp-verify POST {auth}/2fa/verify
totp_disable / AuthRateLimitSlot.TOTP_DISABLE totp ip totp-disable POST {auth}/2fa/disable
totp_regenerate_recovery_codes / AuthRateLimitSlot.TOTP_REGENERATE_RECOVERY_CODES totp ip totp-regenerate-recovery-codes POST {auth}/2fa/recovery-codes/regenerate

The plugin wires these totp_* limiters through TotpRateLimitOrchestrator so totp_verify can reset counters on success or account-state failures while other TOTP routes keep independent budgets.

Reset password counter

For reset_password, failed attempts (invalid token or password) can still consume budget; success may reset the window — see implementation notes in litestar_auth.ratelimit.

Change password counter

For change_password, wrong current-password submissions consume budget, and success resets the window. The default ip_email scope uses the authenticated user's email because ChangePasswordRequest intentionally has no email field.

When an application needs underscore namespaces or other slot-specific deviations, express them directly in endpoint_overrides keyed by AuthRateLimitSlot:

from litestar_auth.ratelimit import (
    AuthRateLimitConfig,
    AuthRateLimitSlot,
    EndpointRateLimit,
    SharedRateLimitConfigOptions,
)

rate_limit_config = AuthRateLimitConfig.from_shared_backend(
    backend,
    options=SharedRateLimitConfigOptions(
        endpoint_overrides={
            AuthRateLimitSlot.FORGOT_PASSWORD: EndpointRateLimit(
                backend=backend,
                scope="ip_email",
                namespace="forgot_password",
            ),
            AuthRateLimitSlot.RESET_PASSWORD: EndpointRateLimit(
                backend=backend,
                scope="ip",
                namespace="reset_password",
            ),
        },
    ),
)

Follow the broader Redis override guidance in Configuration when an existing deployment also needs group-level backend changes, disabled verification routes, or staged TOTP adoption.

Advanced manual wiring

Keep direct AuthRateLimitConfig(..., EndpointRateLimit(...)) construction for exceptional cases where specific endpoints need materially different wiring than the shared-backend builder can express.

from litestar_auth.ratelimit import AuthRateLimitConfig, EndpointRateLimit

rate_limit_config = AuthRateLimitConfig(
    login=EndpointRateLimit(
        backend=login_backend,
        scope="ip_email",
        namespace="login",
        trusted_proxy=True,
        trusted_proxy_hops=2,
    ),
    totp_verify=EndpointRateLimit(backend=totp_backend, scope="ip", namespace="totp-verify"),
)

Further reading