Rediger

429 Too Many Requests: what it means and how to handle it

An API returns 429 Too Many Requests when your app sent more requests than the API allows in a period of time. The request itself is fine. You sent it too often, so the API turned it down. If you wait and send it again, it usually succeeds. The response often includes a Retry-After header that tells you how long to wait. For more information, see RFC 6585, section 4.

Every API implements rate limits differently. The status code, the headers, and the error body all vary, so code that handles one API correctly can mishandle the next.

API Status How to tell how long to wait Watch out for
GitHub 403 or 429 retry-after if present, otherwise x-ratelimit-reset (UTC epoch seconds) when x-ratelimit-remaining is 0, otherwise at least 1 minute A 403 can be a rate limit or a missing permission. Read the headers to tell them apart.
OpenAI 429 retry-after Some 429s, like credit_balance_exhausted, mean that retrying won't help. Check error.code.
Anthropic 429 retry-after A spend-cap 429 has no retry-after and keeps failing until access resumes. An overloaded API returns 529, not 429.
Microsoft Graph 429 Retry-After (seconds) Limits differ per service, for example SharePoint and Outlook.

How to handle a 429

  1. Decide whether to retry at all. If the error says that your quota, credits, or spend limit is used up, retrying won't help. Tell the user, and alert yourself.
  2. Wait as long as the API asks. If the response has Retry-After, wait that long. It's either a number of seconds or an HTTP date. If the API uses rate limit headers instead, like GitHub's x-ratelimit-reset, wait until the reset time.
  3. Otherwise, back off. Without a hint from the API, retry with exponential backoff and random jitter, and stop after a few attempts.
  4. Tell the user what's happening. "Busy, retrying in 5 seconds" beats a spinner that never ends.
  5. Slow down before the next 429. If the API sends rate limit headers, use the remaining count to pace your requests.
async function fetchWithRetry(url, options, attempts = 3) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(url, options);
    if (response.status !== 429 || attempt === attempts) {
      return response;
    }
    const retryAfter = response.headers.get('retry-after');
    const waitMs = retryAfter
      ? (isNaN(retryAfter) ? new Date(retryAfter) - Date.now() : retryAfter * 1000)
      : 2 ** attempt * 1000 + Math.random() * 1000;
    await new Promise(resolve => setTimeout(resolve, Math.max(waitMs, 0)));
  }
}

Many SDKs retry 429s for you. For example, the OpenAI Python SDK retries 2 times by default, and the .NET standard resilience handler retries 3 times and honors Retry-After. When the SDK runs out of retries, your code gets the error, so it still needs a plan.

How to test that your app handles 429

You rarely see a 429 while you develop. The API is fast, you're the only user, and your test data is small. So the way you test 429 handling decides whether you find the bugs before your users do.

Approach What you find What you miss
Wait for production Real failures Everything, until a user hits it
Mock the API in your tests, or let your coding agent write the mock Whether your retry branch runs The API's real status codes, headers, and error bodies, and your SDK's retry policy. Your app also needs a test-only switch to reach the mock.
Call the real API until it throttles you Real behavior You can't trigger a 429 on demand, and you use up your real quota
Intercept your app's real traffic and return 429s on demand Real URLs, your real SDK and retry policy, and the API's own 429 format Nothing in your app changes, so it doesn't test your code in isolation. Keep your unit tests for that.

Try it on your app

Dev Proxy intercepts your app's requests to the APIs you choose and returns 429s, with the API's own headers and error format, while your app keeps calling the real URLs. It also tells you when your app retries before the Retry-After time is up.

Download the preset for the API your app calls, and start Dev Proxy with it:

devproxy config get github-rate-limiting
devproxy --config-file "~dataFolder/configs/github-rate-limiting/.devproxy/devproxyrc.json"
API Preset
GitHub github-rate-limiting
OpenAI openai-throttling
Anthropic anthropic-throttling
Microsoft Graph (OneDrive and SharePoint: /drive, /shares, /sites) microsoft-graph-rate-limiting

Then run your app as usual and watch what it does. To install Dev Proxy, see Set up Dev Proxy.

Next steps

See also