Rate Limiting
Auris enforces rate limits on all API endpoints to protect the platform from abuse, prevent credential stuffing attacks, and ensure fair resource allocation across tenants. This guide explains the rate limiting tiers, how to read rate limit headers, how to handle 429 Too Many Requests responses in your application, and how to configure limits.
Rate Limiting Tiers
Auris applies different rate limits depending on the sensitivity and resource cost of each endpoint. There are four tiers:
Auth Tier (Strict)
Applies to authentication endpoints that handle credentials:
POST /api/auth/login(email/password login)POST /api/auth/signup(user registration)POST /api/auth/token(token exchange and refresh)POST /api/auth/magic-link(magic link initiation)POST /api/auth/passwordless/*(passwordless flows)POST /api/oauth/authenticate(hosted login form submission)
These endpoints have the strictest limits because they are the primary target for credential stuffing and brute-force attacks.
Default limits: Low requests per minute per IP, with additional per-account limits on login endpoints.
Sensitive Tier (Moderate)
Applies to security-critical operations that should not be called frequently:
POST /api/user/2fa/*(2FA enrollment and verification)POST /api/auth/forgot-password(password reset initiation)POST /api/auth/change-password(password change)POST /api/user/phone/*(phone number verification)
Default limits: Moderate requests per minute per IP and per user.
API Tier (Standard)
Applies to general authenticated API endpoints:
GET/POST/PATCH/DELETE /api/users/*GET/POST/PATCH/DELETE /api/roles/*GET/POST/PATCH/DELETE /api/organizations/*GET/POST/PATCH/DELETE /api/applications/*- All other authenticated CRUD endpoints
Default limits: Standard requests per minute per authenticated user.
Public Tier (Relaxed)
Applies to open endpoints that do not require authentication:
GET /.well-known/openid-configuration(OIDC Discovery)GET /.well-known/jwks.json(JWKS)GET /api/public/*(public status pages, verification pages)POST /api/scim/v2/*(SCIM provisioning with bearer token)
Default limits: Generous limits, IP-based only.
Reading Rate Limit Headers
Every response from a rate-limited endpoint includes standard headers that tell you the current state of your rate limit window:
| Header | Type | Description |
|---|---|---|
X-RateLimit-Limit | Integer | Maximum number of requests allowed in the current window |
X-RateLimit-Remaining | Integer | Number of requests remaining before the limit is reached |
X-RateLimit-Reset | Unix timestamp | When the current window resets (seconds since epoch) |
Retry-After | Integer | Seconds until you can retry (only present on 429 responses) |
Example response headers on a successful request:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1737014400
Content-Type: application/jsonExample response headers when rate limited:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1737014400
Retry-After: 47
Content-Type: application/json
{
"ok": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please try again in 47 seconds."
}
}Handling 429 Responses
When your application receives a 429 Too Many Requests response, it should back off and retry after the delay specified in the Retry-After header.
Basic Retry with Backoff
async function callAurisApi(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options)
if (response.status !== 429) {
return response
}
// Read the Retry-After header
const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10)
if (attempt === maxRetries) {
throw new Error(
`Rate limited after ${maxRetries} retries. Retry after ${retryAfter}s.`
)
}
console.warn(
`Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +
`Retrying in ${retryAfter} seconds...`
)
// Wait for the specified duration
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000))
}
throw new Error('Unexpected: exceeded retry loop')
}
// Usage
const response = await callAurisApi(
'https://auth.yourcompany.com/api/users',
{
headers: {
Authorization: `Bearer ${accessToken}`,
'x-tenant': 'your-tenant-id',
},
}
)Exponential Backoff with Jitter
For production systems handling high traffic, use exponential backoff with jitter to avoid thundering herd problems when many clients hit rate limits simultaneously:
async function callWithExponentialBackoff(
url: string,
options: RequestInit,
maxRetries = 5
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options)
if (response.status !== 429) {
return response
}
if (attempt === maxRetries) {
throw new Error('Rate limit exceeded: max retries reached')
}
// Use Retry-After if provided, otherwise exponential backoff
const retryAfter = response.headers.get('Retry-After')
let delay: number
if (retryAfter) {
delay = parseInt(retryAfter, 10) * 1000
} else {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const baseDelay = Math.pow(2, attempt) * 1000
// Add random jitter (0-50% of base delay)
const jitter = Math.random() * baseDelay * 0.5
delay = baseDelay + jitter
}
await new Promise((resolve) => setTimeout(resolve, delay))
}
throw new Error('Unexpected: exceeded retry loop')
}Circuit Breaker Pattern
For applications that make many API calls, implement a circuit breaker to stop sending requests entirely when rate limits are consistently hit:
class AurisCircuitBreaker {
private failureCount = 0
private lastFailure = 0
private state: 'closed' | 'open' | 'half-open' = 'closed'
private readonly threshold = 5
private readonly resetTimeout = 60000 // 1 minute
async call(fn: () => Promise<Response>): Promise<Response> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure > this.resetTimeout) {
this.state = 'half-open'
} else {
throw new Error('Circuit breaker is open. Requests paused.')
}
}
const response = await fn()
if (response.status === 429) {
this.failureCount++
this.lastFailure = Date.now()
if (this.failureCount >= this.threshold) {
this.state = 'open'
console.error(
`Circuit breaker opened after ${this.threshold} rate limit hits. ` +
`Pausing requests for ${this.resetTimeout / 1000}s.`
)
}
throw new Error('Rate limited')
}
// Success — reset the breaker
this.failureCount = 0
this.state = 'closed'
return response
}
}Per-Account Limits on Auth Endpoints
In addition to IP-based rate limits, authentication endpoints enforce per-account limits that work in conjunction with the brute-force protection system:
| Threshold | Action |
|---|---|
| Consecutive failed logins within observation window | Counter incremented |
| Counter reaches lockout threshold (default: 5) | Account locked for escalating duration |
| 1st lockout | 5 minutes |
| 2nd lockout | 30 minutes |
| 3rd lockout | 24 hours |
These limits are per user account and persist across IP addresses. A distributed attack from multiple IPs against the same account will still trigger lockout.
Per-account lockout is separate from IP-based rate limiting. A single IP can be rate-limited while the target account remains unlocked (if the failure count has not reached the threshold), and vice versa.
See the Attack Protection guide for full details on brute-force protection configuration.
SDK Automatic Retry Behavior
The @auris/js SDK includes built-in rate limit handling for token operations:
- Token refresh: If a refresh token request receives a 429, the SDK waits for the
Retry-Afterduration and retries once automatically - Login redirect: The hosted login flow is server-rendered and not subject to client-side rate limits
- API calls via Management client: The Management client does not retry automatically — your application should implement retry logic
import { AurisClient } from '@auris/js'
const auris = new AurisClient({
domain: 'auth.yourcompany.com',
clientId: 'your-client-id',
autoRefresh: true, // SDK handles refresh with built-in retry
})
// Token refresh retry is automatic
// If the refresh endpoint returns 429, the SDK waits and retries
const token = await auris.getAccessToken()For the Management client (M2M), implement your own retry logic:
import { AurisClient } from '@auris/js'
const management = new AurisClient.Management({
domain: 'auth.yourcompany.com',
clientId: 'm2m-client-id',
clientSecret: 'm2m-client-secret',
})
// Management client calls should use your own retry wrapper
const users = await callWithExponentialBackoff(
'https://auth.yourcompany.com/api/users',
{
headers: {
Authorization: `Bearer ${await management.getToken()}`,
'x-tenant': 'your-tenant-id',
},
}
)Console Configuration
Rate limiting settings are viewable in the Auris Console under Settings then Rate Limiting. The Console displays the current configuration for each tier in a read-only view.
The displayed settings include:
- Per-tier limits — Requests per window for each tier (auth, sensitive, api, public)
- Window duration — The sliding window size for each tier
- Current status — Whether rate limiting is active
Rate limit values are configured at the infrastructure level and are displayed in the Console for reference. To request changes to rate limit thresholds for your tenant, contact your Auris administrator or adjust the environment configuration.
Proactive Rate Limit Monitoring
Rather than waiting for 429 responses, monitor the X-RateLimit-Remaining header proactively to throttle your request rate before hitting limits:
class RateLimitAwareClient {
private remaining = Infinity
private resetAt = 0
async request(url: string, options: RequestInit): Promise<Response> {
// If we know we are at the limit, wait proactively
if (this.remaining <= 1 && Date.now() / 1000 < this.resetAt) {
const waitMs = (this.resetAt - Date.now() / 1000) * 1000
console.log(`Proactively waiting ${waitMs}ms to avoid rate limit`)
await new Promise((resolve) => setTimeout(resolve, waitMs))
}
const response = await fetch(url, options)
// Update rate limit state from response headers
const limit = response.headers.get('X-RateLimit-Remaining')
const reset = response.headers.get('X-RateLimit-Reset')
if (limit !== null) this.remaining = parseInt(limit, 10)
if (reset !== null) this.resetAt = parseInt(reset, 10)
return response
}
}Best Practices
Always respect Retry-After. When you receive a 429, the Retry-After header tells you exactly how long to wait. Do not retry before this period.
Use exponential backoff with jitter. For bulk operations or high-throughput scenarios, simple linear retry can cause traffic spikes when multiple clients retry at the same moment. Jitter spreads retries across time.
Implement circuit breakers for critical paths. If your application depends on Auris API calls in the request path (e.g., permission checks), use a circuit breaker to fail gracefully rather than queueing requests while rate limited.
Cache responses where possible. Reduce API calls by caching user profiles, role lists, and permission checks. The @auris/react SDK uses TanStack Query under the hood with a default staleTime that reduces redundant API calls.
Batch operations when available. Use bulk endpoints (e.g., SCIM bulk operations, FGA bulk tuple writes) instead of individual API calls to accomplish the same work with fewer requests.
Monitor rate limit headers in production. Log the X-RateLimit-Remaining value and set up alerts when it drops below a threshold. This gives you early warning before users start seeing 429 errors.
Related Guides
- Attack Protection — Brute-force lockout, CAPTCHA, and the full login security pipeline
- Session Management — Token lifetimes and refresh behavior
- Setting Up Webhooks — Event-driven architecture to reduce polling