Skip to main content
Version 0.1Draft

Rate limits and quotas

Why LuxID rate limits

LuxID applies rate limits to its OAuth 2.0 and OIDC endpoints to protect the shared platform against brute-force attacks, credential stuffing, and runaway automated traffic - including accidental traffic from a Partner's own retry loop. Because LuxID is a single shared identity provider used by many Partners at once, limits protect every Partner from every other Partner's bugs.

Two things follow from that design:

  1. The exact values are deliberately not published. They can be tightened in response to abuse without notice, so do not hardcode assumptions about them. Treat HTTP 429 as the signal and design for it. This is the same position stated in the Endpoints reference.
  2. Your integration must degrade gracefully when throttled, using the Retry-After header and exponential backoff described below.

Which endpoints are limited

All LuxID endpoints are rate-limited. The ones a Partner integration calls routinely:

EndpointTypical callerPrimary limit dimension
GET /mga/sps/oauth/oauth20/authorizeUser's browser (redirect)Per user / per source IP - protects against credential-stuffing and bot traffic on Universal Login
POST /mga/sps/oauth/oauth20/tokenYour serverPer Client ID - protects against code-replay storms and refresh loops
GET /mga/sps/oauth/oauth20/userinfoYour serverPer Client ID and per access token - protects against per-request profile polling
JWKS endpointYour server (should be cached)Per source IP - fetching JWKS on every request may trigger throttling

Per-client vs per-user dimensions

Rate limits are enforced along more than one dimension at the same time:

  • Per Client ID - the aggregate request rate of your Application across all users. This is the dimension you can exhaust with a server-side bug (a token-refresh loop, introspection on every API call, JWKS fetch per request).
  • Per user account - repeated authentication attempts against a single account are throttled regardless of which Partner initiated them. This is an anti-brute-force control; a legitimate integration should never encounter it.
  • Per source IP - anonymous traffic (the authorize endpoint before sign-in, JWKS fetches) is throttled by source address to contain bots and scrapers.

Hitting a per-client limit affects all your users at once, which is why the production guidance below focuses on that dimension.

What a throttled response looks like

When a limit is exceeded, LuxID returns HTTP 429 Too Many Requests, normally with a Retry-After header indicating how many seconds to wait:

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

{
"error": "temporarily_unavailable",
"error_description": "Request rate too high, retry later"
}

Rules for handling it:

  • Honour Retry-After when present. Do not retry earlier.
  • Back off exponentially with jitter when the header is absent.
  • Never tight-loop. A retry loop without backoff turns a momentary throttle into a sustained outage for your own users.
  • Log and alert on 429. In a correctly sized integration, throttling should be rare enough that every occurrence is worth investigating.

Client-side backoff sample (Node.js)

The sample below wraps a server-to-server POST (token exchange, refresh, introspection) with Retry-After-aware exponential backoff and jitter:

async function postToLuxID(url, params, { maxRetries = 4 } = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params),
});

// Retry only on throttling or transient unavailability
if (res.status !== 429 && res.status !== 503) {
return res; // success or a non-retryable error: handle upstream
}

if (attempt >= maxRetries) {
throw new Error(`LuxID still throttling after ${maxRetries} retries`);
}

const retryAfter = Number(res.headers.get('retry-after'));
const waitMs =
Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000 // server-directed wait wins
: Math.min(1000 * 2 ** attempt, 30_000); // exponential fallback, capped at 30 s

const jitterMs = Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, waitMs + jitterMs));
}
}
Do not retry the authorize endpoint

The authorize endpoint is a browser redirect, not a server-to-server call - there is nothing for your server to retry. If a user's browser is throttled there, surface a "please try again in a moment" message rather than re-redirecting in a loop. Backoff applies to the token, UserInfo and JWKS calls your server makes.

For the full operational picture (timeouts, idempotency, single-use authorisation codes, outage behaviour), see Calling LuxID reliably.

Hitting limits in production

If you see 429 responses in production, work through this list before contacting LuxID:

  1. Find the multiplier bug first. The overwhelming majority of production throttling is self-inflicted. The usual suspects:
    • Fetching the JWKS on every request instead of caching it (see Key management).
    • Calling UserInfo on every page load instead of caching claims in your session.
    • Validating access tokens by calling an endpoint on every request instead of validating the JWT signature locally. Token introspection is not available to Partners - see Token introspection.
    • A refresh loop: refreshing on every request, or retrying a failed refresh without backoff.
  2. Check whether the traffic is legitimate. A spike in authorize-endpoint throttling for many users can indicate credential stuffing against your user base rather than a bug on your side.
  3. Flag planned spikes in advance. If a marketing campaign, product launch, or televised event is likely to multiply your authentication volume, tell LuxID before it happens via Partner support, so capacity and limits can be reviewed. See also SLA and support.
  4. Open a ticket with evidence. Include the environment, Client ID, endpoint, timestamp window, and observed 429 rate. The ticket template applies.

Sandbox and UAT differences

UAT (login-uat.luxid.lu) enforces lower rate limits than production. This is deliberate: UAT is shared infrastructure that is not dimensioned for production-scale traffic, and a runaway test loop must not affect other Partners' testing. Do not run load or performance tests at production volumes against UAT.

The behaviour on breach is the same as production: HTTP 429 with Retry-After. Your test harness should honour the header rather than tight-looping - which also means your backoff code gets exercised in UAT for free. See Sandbox environment and Environments.

Updated 2026-07-02