Updated 17 August 2026
Rate limiting authentication endpoints
Login throttling should use independent identifier and network rate limits. Own Auth supplies shared identifier counters for password sign-in, recovery, magic links, email verification, SMS codes, invitations, and API-key creation; the application still owns network limits.
Protect the account and the service independently
A per-account limit stops a distributed attack against one user. A per-network limit stops one source from testing a small number of passwords across thousands of users. These must be independent buckets. A single composite key such as IP + email gives every pair a fresh allowance, which lets an attacker rotate either side of the pair and avoid the intended ceiling.
OWASP recommends checking both a username bucket and an IP or network bucket for login. The identifier bucket should use the same normalized value as the account lookup, even when no account exists. The network bucket should account for trusted proxy configuration, IPv6 address ranges, carrier networks, and corporate egress points. An unverified forwarding header is not a safe client identity.
Permanent lockout based only on failed requests gives an attacker an account-disabling endpoint. Time-bounded throttling can slow guessing without changing account state.
Own Auth supplies the identifier layer
The Own Auth rate-limiting reference lists the fixed limits applied to sensitive operations without separate middleware. The defaults include ten password sign-in attempts per normalized email every ten minutes, five magic-link or email-verification requests per address every ten minutes, five password-reset requests per address every fifteen minutes, five SMS-code requests and ten SMS verifications per phone number every fifteen minutes, ten invitations per organisation per hour, and twenty API-key creations per user or organisation owner per hour.
SMS codes also have a per-code wrong-attempt limit, set to five by default. Each newly requested code receives a fresh per-code allowance, but requests against all codes still share the ten-per-fifteen-minute verification counter for that phone number. Resending therefore replaces the active code without resetting the phone-level budget.
With DATABASE_URL configured, counters live in own_auth_rate_limits in the same Postgres database as the authentication data. Every application instance sees the same counters, and restarts do not clear them. The built-in limits and windows are not configurable in the current release. If the application uses custom storage, its rateLimitStore needs the same durable, shared behavior.
Return a stable public response
RFC 6585 defines 429 Too Many Requests for rate limiting and permits a Retry-After header. Own Auth throws an AuthError with code rate_limited, status code 429, and the safe message Too many attempts. Try again later. It does not currently expose retryAfterMs, so application code should not invent or read that field.
Password-reset, magic-link, and phone-code routes must preserve their generic account response when a limit fires. The response must not include the bucket key, attempts remaining, account existence, or a recipient-specific internal reason.
Observe rate-limit denials
Own Auth audit logs do not currently record rate-limit rejections. With an OpenTelemetry SDK configured, each denial increments own_auth.rate_limit.denial.count with the bounded own_auth.rate_limit.bucket attribute. The metric strips the identifier portion of the counter key before recording the bucket. The default Postgres rate-limit store increments its shared bucket atomically, including concurrent requests at a window boundary.