Skip to content

Rate Limits

SingleSign enforces rate limits to ensure fair usage and platform stability. This page documents the limits for each endpoint, the response format when limits are exceeded, and best practices for staying within bounds.


Per-Endpoint Limits

Rate limits are applied per client ID (or per IP address for unauthenticated endpoints) using a sliding window.

EndpointRate LimitWindow
GET /.well-known/openid-configuration100 requestsPer minute
GET /oauth/authorize60 requestsPer minute
POST /oauth/token120 requestsPer minute
GET /oauth/userinfo120 requestsPer minute
GET /oauth/jwks100 requestsPer minute
POST /oauth/introspect120 requestsPer minute
POST /oauth/revoke60 requestsPer minute

Note: These limits represent reasonable defaults. If your application requires higher throughput, contact SingleSign support to discuss increased limits for your platform.


429 Response

When you exceed a rate limit, SingleSign returns an HTTP 429 Too Many Requests response:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1740000030

Response Body

{
  "error": "too_many_requests",
  "error_description": "Rate limit exceeded. Please retry after 30 seconds."
}

Response Headers

HeaderTypeDescription
Retry-AfterintegerNumber of seconds to wait before retrying.
X-RateLimit-LimitintegerThe maximum number of requests allowed in the current window.
X-RateLimit-RemainingintegerThe number of requests remaining in the current window.
X-RateLimit-ResetintegerUnix timestamp when the rate limit window resets.

Handling Rate Limits

Exponential Backoff

When you receive a 429 response, wait before retrying. The recommended strategy is exponential backoff with jitter:

async function fetchWithRetry(
  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;
    }

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

    // Use Retry-After header if available
    const retryAfter = response.headers.get("Retry-After");
    let waitMs: number;

    if (retryAfter) {
      waitMs = parseInt(retryAfter, 10) * 1000;
    } else {
      // Exponential backoff: 1s, 2s, 4s + random jitter
      waitMs = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
    }

    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }

  throw new Error("Unreachable");
}

Python Example

import time
import random
import requests

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

        if response.status_code != 429:
            return response

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

        retry_after = response.headers.get("Retry-After")
        if retry_after:
            wait = int(retry_after)
        else:
            wait = (2 ** attempt) + random.uniform(0, 1)

        time.sleep(wait)

Best Practices

1. Cache Tokens

The most effective way to reduce API calls is to cache your access tokens and reuse them until they expire.

let cachedToken: { token: string; expiresAt: number } | null = null;

async function getAccessToken(): Promise<string> {
  if (cachedToken && cachedToken.expiresAt > Date.now() + 60_000) {
    return cachedToken.token;
  }

  const tokens = await exchangeCodeForTokens();
  cachedToken = {
    token: tokens.access_token,
    expiresAt: Date.now() + tokens.expires_in * 1000,
  };

  return cachedToken.token;
}

2. Cache Discovery and JWKS Responses

The discovery endpoint (/.well-known/openid-configuration) and JWKS endpoint (/oauth/jwks) return data that changes infrequently. Cache these responses for at least 1 hour and refresh on a schedule.

let discoveryCache: { data: any; fetchedAt: number } | null = null;
const CACHE_TTL = 60 * 60 * 1000; // 1 hour

async function getDiscovery(): Promise<any> {
  if (discoveryCache && Date.now() - discoveryCache.fetchedAt < CACHE_TTL) {
    return discoveryCache.data;
  }

  const response = await fetch(
    `https://api.singlesign.com/.well-known/openid-configuration?platform_guid=${platformGuid}`
  );
  const data = await response.json();

  discoveryCache = { data, fetchedAt: Date.now() };
  return data;
}

3. Use Refresh Tokens Instead of Re-Authenticating

Instead of sending users through the full authorization flow when their access token expires, use a refresh token to get a new access token. This reduces calls to both /authorize and /token.

4. Batch Operations

If your application needs to make many API calls, batch them where possible. For example, extract all needed claims from the /oauth/userinfo response in a single call rather than making repeated requests.

5. Monitor Rate Limit Headers

Proactively check X-RateLimit-Remaining to throttle your requests before hitting the limit:

function shouldThrottle(response: Response): boolean {
  const remaining = parseInt(response.headers.get("X-RateLimit-Remaining") || "999", 10);
  return remaining < 5;
}

6. Avoid Token Introspection on Every Request

If your server validates tokens on every API request, prefer local JWT validation (verify the signature and check exp) over calling /oauth/introspect. Reserve introspection for cases where you need real-time revocation checks.


Rate Limit Scope

ScopeDescription
Per Client IDAuthenticated endpoints (/token, /userinfo, /introspect, /revoke) are limited per client_id.
Per IPUnauthenticated endpoints (/authorize, discovery, JWKS) are limited per source IP address.

If you operate multiple instances of your application behind a load balancer, all instances share the same client-level rate limit.


Increasing Your Limits

If your application legitimately needs higher rate limits:

  1. Review the best practices above to ensure you're not making unnecessary requests.
  2. Contact SingleSign support with:
    • Your platform_guid and client_id.
    • Which endpoint(s) need higher limits.
    • Your expected request volume and traffic pattern.
    • Why caching and other optimizations are insufficient.

Next Steps

Last updated: 2026-02-16