Skip to Content
GuidesRate Limiting

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:

HeaderTypeDescription
X-RateLimit-LimitIntegerMaximum number of requests allowed in the current window
X-RateLimit-RemainingIntegerNumber of requests remaining before the limit is reached
X-RateLimit-ResetUnix timestampWhen the current window resets (seconds since epoch)
Retry-AfterIntegerSeconds 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/json

Example 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:

ThresholdAction
Consecutive failed logins within observation windowCounter incremented
Counter reaches lockout threshold (default: 5)Account locked for escalating duration
1st lockout5 minutes
2nd lockout30 minutes
3rd lockout24 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-After duration 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.