Rate Limits

UniAuth enforces rate limits on all API endpoints to protect the platform from abuse, prevent brute-force attacks, and ensure fair usage for all integrations. This page documents the rate limit configuration for every endpoint, how limits are enforced, and best practices for staying within them.

Overview

Rate limits are enforced using a sliding window algorithm. Each window is 15 minutes long and limits are applied per IP address. When you make a request, UniAuth checks how many requests the originating IP has made to that specific endpoint within the current 15-minute window. If the count exceeds the limit, the request is rejected with a 429 Too Many Requests response.

Sensitive authentication endpoints have stricter limits than general-purpose endpoints. This design allows normal application usage to proceed unimpeded while making automated attacks impractical.

Note: Rate limits apply to all callers equally, including authenticated requests. If you are building a backend integration that calls UniAuth APIs on behalf of multiple users, all requests from your server IP count toward the same limit.

Per-Endpoint Limits

The following table lists the rate limit for each endpoint. All windows are 15 minutes.

EndpointLimitWindow
POST /api/auth/login10 requests15 minutes
POST /api/auth/register5 requests15 minutes
POST /api/auth/forgot-password5 requests15 minutes
POST /api/auth/2fa/send-code10 requests15 minutes
POST /api/oauth/token30 requests15 minutes
GET /api/oauth/authorize20 requests15 minutes
POST /api/oauth/introspect30 requests15 minutes
POST /api/oauth/end-session10 requests15 minutes
All other /api/* endpoints100 requests15 minutes

The limits above are defaults. Self-hosted deployments can adjust these thresholds through environment configuration. Contact your administrator if you need higher limits for a specific use case.

Rate Limit Response

When a rate limit is exceeded, UniAuth returns the following response:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 900

{
  "success": false,
  "message": "Too many requests. Please try again later."
}

Response Headers

HeaderDescription
Retry-AfterNumber of seconds until the current rate limit window resets. Wait at least this long before retrying.

Handling Rate Limits

Your application should gracefully handle rate limit responses by implementing retry logic with exponential backoff. Below are examples in multiple languages.

JavaScript / TypeScript

async function fetchWithRetry(url, options, maxRetries = 3) {
  let attempt = 0;

  while (attempt < maxRetries) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    attempt++;
    if (attempt >= maxRetries) {
      throw new Error("Rate limit exceeded after maximum retries");
    }

    // Use Retry-After header, or fall back to exponential backoff
    const retryAfter = response.headers.get("Retry-After");
    const delay = retryAfter
      ? parseInt(retryAfter, 10) * 1000
      : Math.min(1000 * Math.pow(2, attempt), 30000);

    console.log(`Rate limited. Retrying in ${delay / 1000}s (attempt ${attempt}/${maxRetries})`);
    await new Promise(resolve => setTimeout(resolve, delay));
  }
}

// Usage
const response = await fetchWithRetry("https://your-uniauth-instance.com/api/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code: authorizationCode,
    redirect_uri: "https://your-app.com/callback",
    client_id: "your-client-id",
    client_secret: "your-client-secret",
    code_verifier: codeVerifier,
  }),
});

Python

import time
import requests

def fetch_with_retry(url, method="GET", max_retries=3, **kwargs):
    for attempt in range(max_retries):
        response = requests.request(method, url, **kwargs)

        if response.status_code != 429:
            return response

        if attempt + 1 >= max_retries:
            raise Exception("Rate limit exceeded after maximum retries")

        # Use Retry-After header or exponential backoff
        retry_after = response.headers.get("Retry-After")
        if retry_after:
            delay = int(retry_after)
        else:
            delay = min(2 ** (attempt + 1), 30)

        print(f"Rate limited. Retrying in {delay}s (attempt {attempt + 1}/{max_retries})")
        time.sleep(delay)

# Usage
response = fetch_with_retry(
    "https://your-uniauth-instance.com/api/oauth/token",
    method="POST",
    data={
        "grant_type": "authorization_code",
        "code": authorization_code,
        "redirect_uri": "https://your-app.com/callback",
        "client_id": "your-client-id",
        "client_secret": "your-client-secret",
        "code_verifier": code_verifier,
    },
)

Go

package main

import (
    "fmt"
    "net/http"
    "strconv"
    "time"
    "math"
)

func fetchWithRetry(req *http.Request, maxRetries int) (*http.Response, error) {
    client := &http.Client{}

    for attempt := 0; attempt < maxRetries; attempt++ {
        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }

        if resp.StatusCode != http.StatusTooManyRequests {
            return resp, nil
        }
        resp.Body.Close()

        if attempt+1 >= maxRetries {
            return nil, fmt.Errorf("rate limit exceeded after %d retries", maxRetries)
        }

        retryAfter := resp.Header.Get("Retry-After")
        var delay time.Duration
        if seconds, err := strconv.Atoi(retryAfter); err == nil {
            delay = time.Duration(seconds) * time.Second
        } else {
            delay = time.Duration(math.Min(math.Pow(2, float64(attempt+1)), 30)) * time.Second
        }

        fmt.Printf("Rate limited. Retrying in %v (attempt %d/%d)\n", delay, attempt+1, maxRetries)
        time.Sleep(delay)
    }

    return nil, fmt.Errorf("unreachable")
}

Distributed Rate Limiting

By default, rate limits are enforced per application instance using in-memory storage. This works well for single-server deployments but can lead to inconsistent enforcement when running multiple instances behind a load balancer.

When Redis is configured, UniAuth automatically switches to distributed rate limiting. Limits are enforced globally across all instances using a Redis-backed sliding window algorithm. This ensures that a client sending requests to different instances is still correctly rate-limited against the same counter.

ModeStorageBehavior
Single instance (default)In-memoryLimits enforced per instance. Counters reset when the server restarts.
Multi-instance (Redis)RedisLimits enforced globally across all instances. Counters persist across restarts and are shared.

To enable Redis-backed rate limiting, configure the REDIS_URL environment variable in your deployment. See the Configuration guide for details.

Best Practices

Follow these guidelines to minimize rate limit issues in your integration:

  • Cache token responses. Access tokens are valid for 1 hour. Store the token and its expiration time, and reuse it for subsequent API calls rather than requesting a new token for every request.
  • Use refresh tokens instead of re-authenticating. When an access token expires, exchange the refresh token for a new one. This uses the token endpoint (30 requests/15 min) rather than the login endpoint (10 requests/15 min), and does not require user interaction.
  • Implement client-side throttling. Before sending requests, maintain a local counter and delay or queue requests when approaching the limit. This provides a better user experience than hitting the rate limit and retrying.
  • Spread batch operations over time. If you need to perform bulk operations (such as provisioning multiple users), spread the requests across the rate limit window rather than sending them all at once. Use the SCIM API for bulk user provisioning.
  • Use webhooks for real-time updates. Instead of polling the API for changes, configure webhooks to receive push notifications when events occur. This eliminates the need for repeated API calls.
  • Respect the Retry-After header. When you receive a 429 response, always use the Retry-After header value rather than a fixed delay. This ensures you resume at the earliest possible time without unnecessary waiting.
  • Avoid retry storms. If multiple instances of your application are hitting rate limits simultaneously, add jitter to your retry delays. Instead of all instances retrying at the same time, each should add a random offset:
// Add jitter to prevent retry storms
const baseDelay = parseInt(retryAfter, 10) * 1000;
const jitter = Math.random() * 2000; // 0-2 second random jitter
await new Promise(resolve => setTimeout(resolve, baseDelay + jitter));

Rate Limits vs. Account Lockout

Rate limits and account lockout are separate security mechanisms that work in parallel:

MechanismScopeTriggerHTTP Status
Rate LimitPer IP address, per endpointToo many requests from one IP regardless of outcome429
Account LockoutPer user accountToo many failed login attempts for a specific account423

A single attacker can trigger both: the rate limit (by sending many requests from one IP) and the account lockout (by failing authentication for a specific user). Your error handling should distinguish between the two status codes and display appropriate messages to the user. See the Error Reference for details on both response formats.

Related documentation: Error Reference · Security Guide · Token Reference · Webhooks