developers

Why You're Getting 429s in Production Using Auth0 and How to Stop Them

Getting Auth0 429 status code errors in production? They almost always trace back to APIs rate limit. Here are the two causes and how to fix them.

If you're getting 429s from Auth0 in production, API’s rate limit is almost certainly involved. The errors could feel intermittent because they correlate with traffic spikes, which makes them look harder to diagnose than they are.

Most of the time it comes down to one of two things: you're calling Auth0’s API on every request to fetch data that should have been in the token, or you're fetching a new M2M token on every request instead of caching the one you already have. This post covers both, how to tell which one you're in, and what the correct architecture looks like.

Looking into the Management API and its Rate Limits

The Management API is one of the most used APIs from Auth0, and the rate limits the go per tenant, but also varies by plan. The rate limit policy docs have the full breakdown by tier. The important thing to know is that the limits are tighter than most developers expect.

Responses include x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset headers on every request, so you should log those, otherwise you won't know you're approaching the limit until you've already hit it.

Cause 1: You're Calling the Management API Per-Request

If every API request your server handles calls the Management API to fetch the user's roles, permissions, or custom metadata before processing anything, the design may appear fine at low traffic. As traffic grows, those calls can quickly exhaust the tenant's Management API limits.

The fix is moving that data into the token at login instead of fetching it at runtime. A Post-Login Action runs once when the user logs in and can attach whatever you need directly to the access token. Your API then reads from the token on every request with no external call needed.

exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://your-app.com';

  // Roles are available on the event — no Management API call needed
  const roles = event.authorization?.roles ?? [];
  api.accessToken.setCustomClaim(`${namespace}/roles`, roles);

  // User metadata is also on the event
  const plan = event.user.app_metadata?.plan ?? 'free';
  api.accessToken.setCustomClaim(`${namespace}/plan`, plan);
};

event.authorization.roles and event.user.user_metadata are both available in the Action context without any additional API call. The token then carries this data on every subsequent request, and your API reads it directly from the JWT.

Token claims are a good fit for authorization data that can safely be treated as a snapshot for the lifetime of the access token. If a role removal, suspension, or entitlement change must take effect immediately, use short-lived access tokens and/or enforce that decision against a live, authoritative system.

Cause 2: You're Fetching a New M2M Token on Every Request

M2M tokens are valid for 24 hours by default and are designed to be cached and reused. Fetching a new M2M token on every request creates unnecessary traffic to Auth0’s token endpoint. It adds latency, can trigger rate limits for that endpoint, and makes the overall system less reliable.

The fix is caching the token in memory and only requesting a new one when it's close to expiring. A buffer of 60 seconds before expiry is a safe threshold. Let’s look at this example in Javascript:

let tokenCache = null;

async function getM2MToken() {
  const now = Math.floor(Date.now() / 1000);

  if (tokenCache && tokenCache.expiresAt > now + 60) {
    return tokenCache.accessToken;
  }

  const response = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: process.env.AUTH0_CLIENT_ID,
      client_secret: process.env.AUTH0_CLIENT_SECRET,
      audience: process.env.AUTH0_AUDIENCE,
      grant_type: 'client_credentials',
    }),
  });

  const data = await response.json();
  if (!response.ok) {
    const body = await response.text();
    throw new Error(`Failed to fetch M2M token: ${response.status} ${body}`);
}

  tokenCache = {
    accessToken: data.access_token,
    expiresAt: now + data.expires_in,
  };

  return tokenCache.accessToken;
}

Every call goes through getM2MToken(). If the cached token is still valid with more than 60 seconds left, it returns immediately. Otherwise it fetches a new one, stores it with its expiry, and returns that.

Each service you have should have its own client, to have more control over least privilege access, credential rotation, and observability.

Data That Still Requires a Live Management API Call

As mentioned before, not everything can live in the token. Things like email verification status before granting access to a sensitive feature, org membership when an admin just added a user mid-session, MFA enrollment status for step-up auth are things that change between requests and need to be current.

For these cases, the right pattern is a server-side cache with a short TTL rather than a direct Management API call on every request. A 30 to 60 second TTL is usually enough. It reduces call volume significantly while keeping data fresh enough for most use cases.

Retry and Backoff When You Do Get a 429 Status Code

When a 429 does happen, the response includes a x-ratelimit-reset header with the Unix timestamp of when the limit resets. Retry after that time. An example of this could look as follows:

async function callWithRetry(fn) {
  const response = await fn();

  if (response.status === 429) {
    const resetAt = parseInt(response.headers.get('x-ratelimit-reset'), 10);
    const waitMs = (resetAt - Math.floor(Date.now() / 1000)) * 1000;
    await new Promise(resolve => setTimeout(resolve, Math.max(waitMs, 0)));
    return fn();
  }

  return response;
}

Don't use exponential backoff here the way you would for network errors. The reset time is deterministic, so waiting longer than necessary just delays your users without any benefit.

Quick Diagnostic Checklist

  1. Are you calling the Management API on every request? Which data are you fetching? Can it safely be represented as a token claim?
  2. Are you caching your M2M token?
  3. For data that must be current, do you have a server-side cache with a short TTL?
  4. When a 429 does hit, are you reading x-ratelimit-reset for the retry time

Frequently Asked Questions

A 429 from Auth0 means you've hit a rate limit, and in production it almost always traces back to the Management API. It usually comes down to one of two patterns: calling the Management API on every request to fetch data that should have been in the token, or fetching a new M2M token on every request instead of caching the one you already have. The errors feel intermittent because they correlate with traffic spikes, but the underlying architecture problem is constant.
Auth0 rate limits the Management API per tenant, and the exact limits vary by plan. They're tighter than most developers expect, so you can hit them under normal traffic growth without realizing it. Every Management API response includes x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset headers. Log those so you know when you're approaching the limit instead of finding out only after you've hit it. The full breakdown by tier is in Auth0's rate limit policy docs.
Move the data into the access token at login instead of fetching it at runtime. A Post-Login Action runs once when the user logs in and can attach roles, permissions, or custom metadata directly to the token as custom claims, using data already available on the event object with no Management API call. Your API then reads those claims straight from the JWT on every request, with zero external calls. This works for authorization data you can safely treat as a snapshot for the lifetime of the token.
Yes. M2M tokens are valid for 24 hours by default and are designed to be cached and reused. Fetching a new one on every request creates unnecessary traffic to Auth0's token endpoint, adds latency, can trip the rate limit on that endpoint, and makes your system less reliable. Cache the token in memory and only request a new one when the current one is close to expiring. A 60-second buffer before expiry is a safe threshold.
Token claims are a good fit for authorization data you can treat as a snapshot for the lifetime of the access token, like roles or plan tier. Data that must be current, such as email verification status, org membership after an admin change mid-session, or MFA enrollment for step-up auth, needs to be live. For those cases, don't call the Management API on every request. Put a server-side cache with a short TTL in front of it. A 30 to 60 second TTL cuts call volume significantly while keeping the data fresh enough for most use cases.
Read the x-ratelimit-reset header, which is the Unix timestamp of when the limit resets, and retry after that time. Don't use exponential backoff the way you would for network errors. The reset time is deterministic, so Auth0 is telling you exactly when it's safe to try again, and waiting any longer than that just delays your users with no benefit.

About the author

Carla Urrea Stabile

Carla Urrea Stabile

Staff Developer Advocate

I've been working as a software engineer since 2014, particularly as a backend engineer and doing system design. I consider myself a language-agnostic developer but if I had to choose, I like to work with Ruby and Python.

After realizing how fun it was to create content and share experiences with the developer community I made the switch to Developer Advocacy. I like to learn and work with new technologies.

When I'm not coding or creating content you could probably find me going on a bike ride, hiking, or just hanging out with my dog, Dasha.

View profile