Skip to content

Guide

How to handle OpenAI 429 rate-limit errors in production

An OpenAI 429 means one of six things, and only two of them, a rate limit and slow_down, are fixed by waiting: read error.code, retry those after retry-after or with exponential backoff and jitter, and stop at once on the four billing and quota codes. The official SDKs retry every 429 twice by default whatever its code, so turn their retries off wherever you handle the error yourself.

By INFRO Editorial

Last updated

Last checked

On this page
  1. The six 429s and what each one means
  2. What the response tells you
  3. What the SDK already does, and why to turn it off
  4. Retry only the 429s that can succeed
  5. Slow down before OpenAI has to send a 429
  6. When waiting is not an option: fail over
  7. What to do, case by case
  8. The same behaviour through INFRO
  9. Questions
  10. Sources

The six 429s and what each one means

OpenAI's error-codes guide lists six different errors behind the same status. Two are about speed. Four are about money or quota, and OpenAI says plainly that retrying those won't restore access.

error.codeRetry?Message, and what to do
noneYesRate limit reached for requests: Requests or tokens per minute for the organization or project are used up. Wait for retry-after, then retry.
slow_downYesSlow down: Traffic rose too fast, even inside your limits. Retry with backoff and ramp up more slowly.
credit_balance_exhaustedNoCredit balance exhausted: The account's paid-in balance is used up. Retrying cannot succeed; someone has to add funds.
organization_spend_limit_exceededNoOrganization spend limit reached: The organization's enforced monthly spend limit, across all projects. Fails until the limit is raised or the month resets.
project_spend_limit_exceededNoProject spend limit reached: The project's enforced monthly spend limit. Other projects keep working.
organization_usage_limit_exceededNoOrganization usage limit reached: A usage limit OpenAI assigned to the organization. Fails until OpenAI raises it.
From OpenAI's error-codes guide. Older answers still name insufficient_quota; it can still appear as the error type on billing errors, so treat it like the four billing codes.

Two things follow. A retry loop that does not read the code turns "you hit your spend limit" into minutes of retries and a timeout. And because failed requests count toward your per-minute limit, a tight retry loop on a real rate limit makes the 429 last longer.

What the response tells you

Every response, successful or not, carries the state of your limits. The values below are the examples in OpenAI's rate-limits guide:

Rate-limit headers on an OpenAI response
x-ratelimit-limit-requests: 60
x-ratelimit-limit-tokens: 150000
x-ratelimit-remaining-requests: 59
x-ratelimit-remaining-tokens: 149984
x-ratelimit-reset-requests: 1s
x-ratelimit-reset-tokens: 6m0s
retry-after: 56
  • retry-after is the minimum number of seconds to wait before retrying a temporary rate-limit error, when present. When it is missing, use exponential backoff with jitter.
  • The reset headers are durations (1s, 6m0s), not timestamps.
  • Limits are set per organization and per project, never per user. If one project's batch job shares a key's project with your live traffic, it shares the limit too.
  • The SDK puts the body's message, type, code and param on the error it throws, along with the response headers and the x-request-id you quote to OpenAI support.

What the SDK already does, and why to turn it off

Both official SDKs retry connection errors, 408, 409, 429 and 5xx twice by default, with a short exponential backoff. They honour retry-after (and retry-after-ms): the Node SDK up to 60 seconds, the Python SDK up to 120. That default is reasonable for a script. In production it has three problems:

  1. It retries all six 429s, including the four that cannot succeed. The only exception is a response carrying x-should-retry: false.
  2. It cannot fail over: two more attempts against a provider that is rate limiting you is two more requests counted against the limit.
  3. The default timeout is 10 minutes, far longer than a user will wait.

Set maxRetries: 0 (Node) or max_retries=0 (Python) where you handle errors yourself, and set a timeout that matches what the caller can wait.

Retry only the 429s that can succeed

This reads the code before deciding, waits what OpenAI asks for, and gives up instead of sleeping when the wait is longer than a user should see. Both files are run against OpenAI's documented responses on every build.

import OpenAI, { APIError } from "openai";

// Turn the SDK's own retries off: it retries every 429, including the ones
// that cannot succeed. The loop below reads the error code first.
const client = new OpenAI({ maxRetries: 0, timeout: 30_000 });

// 429s that no amount of waiting fixes: billing, spend limits and quota.
const QUOTA_CODES = new Set([
  "credit_balance_exhausted",
  "organization_spend_limit_exceeded",
  "project_spend_limit_exceeded",
  "organization_usage_limit_exceeded",
  "insufficient_quota",
]);

export function isRetryable(err: unknown): err is APIError {
  if (!(err instanceof APIError)) return false;
  if (err.status === undefined) return true; // connection error or timeout
  if (err.status === 429) return !QUOTA_CODES.has(String(err.code)) && !QUOTA_CODES.has(String(err.type));
  return err.status === 408 || err.status === 409 || err.status >= 500;
}

// Wait what the server asks for; otherwise exponential backoff with jitter.
export function retryDelayMs(err: APIError, attempt: number): number {
  const header = (name: string) => Number(err.headers?.get(name));
  if (header("retry-after-ms") > 0) return header("retry-after-ms");
  if (header("retry-after") > 0) return header("retry-after") * 1000;
  const ceiling = Math.min(500 * 2 ** attempt, 8_000);
  return ceiling / 2 + Math.random() * (ceiling / 2);
}

export async function withRetries<T>(
  call: () => Promise<T>,
  { maxAttempts = 5, maxWaitMs = 20_000 } = {},
): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await call();
    } catch (err) {
      if (!isRetryable(err) || attempt + 1 >= maxAttempts) throw err;
      const wait = retryDelayMs(err, attempt);
      // A user is waiting: past this, fail over or fail fast instead of sleeping.
      if (wait > maxWaitMs) throw err;
      await new Promise((resolve) => setTimeout(resolve, wait));
    }
  }
}

try {
  const completion = await withRetries(() =>
    client.chat.completions.create({
      model: "gpt-5.4-mini",
      messages: [{ role: "user", content: "Summarize this ticket in one line." }],
    }),
  );
  console.log(completion.choices[0]?.message.content);
} catch (err) {
  if (err instanceof APIError) {
    console.error(`gave up: ${err.status} ${err.code ?? err.type} (request ${err.requestID})`);
    process.exitCode = 1;
  } else {
    throw err;
  }
}
Tested: a plain 429 and a slow_down are retried and succeed on the third attempt; project_spend_limit_exceeded is not retried; five 5xx in a row end in a clear error.

Slow down before OpenAI has to send a 429

For batch jobs and workers that control their own pace, the cheapest 429 is the one you never get. Read the remaining requests and tokens on every response and pause new work when they run low.

import OpenAI from "openai";

const client = new OpenAI();

// .withResponse() returns the parsed body and the raw response, headers included.
const { data, response } = await client.chat.completions
  .create({
    model: "gpt-5.4-mini",
    messages: [{ role: "user", content: "Classify: 'refund not received'" }],
  })
  .withResponse();

const remainingRequests = Number(response.headers.get("x-ratelimit-remaining-requests"));
const remainingTokens = Number(response.headers.get("x-ratelimit-remaining-tokens"));
const resetRequests = response.headers.get("x-ratelimit-reset-requests"); // e.g. "1s"

console.log(data.choices[0]?.message.content);
console.log({ remainingRequests, remainingTokens, resetRequests });

// Slow a batch job down before OpenAI has to send a 429.
if (remainingRequests < 5 || remainingTokens < 2_000) {
  console.log(`near the limit: pause new work until the window resets (${resetRequests})`);
}

When waiting is not an option: fail over

A user-facing request cannot sleep for 56 seconds. When OpenAI is rate limiting or failing you, send the request to a second provider instead, and keep three rules.

import OpenAI, { APIError } from "openai";
import Anthropic from "@anthropic-ai/sdk";

// No retries on OpenAI: a rate-limited provider stays limited for seconds, so
// the fallback IS the retry. Both clients read their keys from the environment.
const openai = new OpenAI({ maxRetries: 0, timeout: 20_000 });
const anthropic = new Anthropic({ maxRetries: 1, timeout: 20_000 });

// Never fail over on these: your own spend or quota limit said stop, and
// moving the traffic to another provider would spend around it.
const QUOTA_CODES = new Set([
  "credit_balance_exhausted",
  "organization_spend_limit_exceeded",
  "project_spend_limit_exceeded",
  "organization_usage_limit_exceeded",
  "insufficient_quota",
]);

function shouldFailOver(err: unknown): boolean {
  if (!(err instanceof APIError)) return false;
  if (err.status === undefined) return true; // connection error or timeout
  if (err.status === 429) return !QUOTA_CODES.has(String(err.code)) && !QUOTA_CODES.has(String(err.type));
  return err.status === 408 || err.status >= 500;
}

export async function answer(prompt: string): Promise<{ text: string; servedBy: string }> {
  try {
    const res = await openai.chat.completions.create({
      model: "gpt-5.4-mini",
      messages: [{ role: "user", content: prompt }],
    });
    return { text: res.choices[0]?.message.content ?? "", servedBy: "openai" };
  } catch (err) {
    if (!shouldFailOver(err)) throw err;
    const res = await anthropic.messages.create({
      model: "claude-haiku-5-5",
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }],
    });
    const text = res.content.map((block) => (block.type === "text" ? block.text : "")).join("");
    return { text, servedBy: "anthropic" };
  }
}

const { text, servedBy } = await answer("Draft a two-line status update for a delayed order.");
// Log who served it: a fallback that nobody can see becomes a silent quality change.
console.log(`[${servedBy}] ${text}`);
Tested: a 429 goes to Anthropic after one OpenAI attempt; a spend-limit 429 does not fail over; a normal answer stays on OpenAI.
  • Never fail over on a spend or quota code. Your own limit said stop; sending the traffic to another provider spends around it.
  • Fail over at once instead of retrying. A rate-limited provider stays limited for seconds; the fallback is the retry.
  • Log who served each request. A fallback to a different model is a quality change nobody sees unless the response says where it came from.

What to do, case by case

Wait retry-after, then retry
if the 429 has no code, or slow_down, and the caller can wait that long.
Fail over to another provider
if the 429 is a rate limit or the error is a 5xx or timeout, and a user is waiting.
Stop and alert a human
if the code is credit_balance_exhausted, organization_spend_limit_exceeded, project_spend_limit_exceeded or organization_usage_limit_exceeded.
Pace the job
if a batch job sees x-ratelimit-remaining-* approach zero.
Split traffic across projects
if a batch job and live traffic share one project's limit.

Questions

Should I retry insufficient_quota or a spend-limit 429?
No. OpenAI says retrying billing, spend or quota errors won't restore access. Stop, alert someone, and show the user a clear message.
How long should I wait after an OpenAI 429?
The number of seconds in retry-after when the response has one. When it does not, use exponential backoff with jitter, starting around half a second and capped at a few seconds.
Does the OpenAI SDK retry 429 errors automatically?
Yes. The Node and Python SDKs retry 429s twice by default, whatever the error code, and honour retry-after. Set maxRetries to 0 where you handle the error yourself.

Sources

Every statement about another company on this page comes from that company's own page, opened on the date shown. Prices and defaults change; check the source before you decide.

  1. 1.developers.openai.com/api/docs/guides/error-codes (opens in a new tab) — checked Oct 24, 2026.
    • OpenAI documents six distinct 429 errors: the plain rate limit, slow_down, and four billing or quota codes.
    • A 503 with code server_is_overloaded means the model is temporarily overloaded; OpenAI says to follow Retry-After.
  2. 2.developers.openai.com/api/docs/guides/rate-limits (opens in a new tab) — checked Oct 24, 2026.
    • Every response carries x-ratelimit-limit, -remaining and -reset headers for requests and tokens.
    • OpenAI documents retry-after as the minimum seconds to wait before retrying a temporary rate-limit error, and says to fall back to exponential backoff with jitter when it is missing.
    • Failed requests count toward the per-minute limit, so resending in a tight loop makes a 429 last longer.
    • OpenAI sets rate limits per organization and per project, not per user.
  3. 3.github.com/openai/openai-node/blob/master/src/core/error.ts (opens in a new tab) — checked Oct 24, 2026.
    • The SDKs expose an error body's message, type, code and param on the thrown error, with the response headers and the x-request-id.
  4. 4.github.com/openai/openai-python/blob/main/README.md (opens in a new tab) — checked Oct 24, 2026.
    • The official SDKs retry connection errors, 408, 409, 429 and 5xx twice by default, with a short exponential backoff.
    • The SDKs' default request timeout is 10 minutes.
  5. 5.github.com/openai/openai-node/blob/master/src/client.ts (opens in a new tab) — checked Oct 24, 2026.
    • The SDKs retry every 429, whatever its code, unless the response carries x-should-retry: false.
    • The Node SDK honours retry-after-ms and retry-after up to 60 seconds and ignores longer values, falling back to its own backoff; the Python SDK honours them up to 120 seconds.
  6. 6.github.com/openai/openai-python/blob/main/src/openai/_constants.py (opens in a new tab) — checked Oct 24, 2026.
    • The Python SDK's longest honoured retry-after is two minutes.

The team building INFRO, a BYOK-first control plane for production AI. Articles are written from the engineering work on the gateway and cite their external sources.