The failure modes
Don’t blanket-retry. Retrying caller errors burns your rate-limit budget for nothing.
Rate limiting: wait out the window
The limit is a 60-second sliding window per workspace. When you exceed it:Retry-After header, and there is one thing you need to know before you write a retry loop: a refused request is counted into the window too. Every retry inside those 60 seconds occupies another slot and pushes your recovery further out. A tight backoff loop can keep a workspace refused indefinitely.
So don’t back off in milliseconds. Either wait out the window, or — much better — stay under the limit on purpose (see below).
python
Exponential backoff with jitter (for 5xx)
For upstream transients, where a repeat is safe — aGET, or a POST you sent an Idempotency-Key on:
python
+ random()) is critical — without it, every client retries at the same instant and you get a stampede.
job_id in an error body is not a failure
If an error body carries a job_id, the work exists server-side and is billed whether or not you wait for it — a media generation that outran the gateway’s wait budget (504), or speech that was generated and settled but could not be read back (500 artifact_unreadable). Do not resend the request. Collect the result:
/v1/images path. Resending instead starts and bills a second generation for a result you already own.
Don’t retry 4xx
400/401/404/422 are deterministic — retrying just wastes your rate-limit budget and money. Fix the request, then resubmit. Common offenders:
- Wrong model slug → check
GET /v1/models - Missing required parameter → check the endpoint reference
- Image too large → resize before resending
- Malformed JSON → fix the producer
SDK-level retries
The OpenAI SDK retries429s and 5xx automatically:
python
403 is a hard client error to the OpenAI SDK, so it is not retried and not backed off — a caller leaning on max_retries to ride out bursts gets an immediate, unretried failure instead. And its 5xx retry is the blind one the warning above is about. For rate limiting, handle the 403 yourself; for bursts, queue.
Our own TypeScript SDK applies the rules on this page for you: it does not retry 403 rate_limit_exceeded, does not retry a 5xx on a billed POST, does retry 409 upload_in_progress, and collects a job_id rather than resending.
Stay under the limit on purpose
Reactive retry is the floor. Proactive limiting is the ceiling. Token-bucket on your side, capped at ~80% of the key’s RPM:python
Fallback chains: the better answer for production
Per-call retries help, but fallback chains help more. Configure once:gpt-4o — OpenAI’s own 429, or a 5xx — is invisible to your code: the gateway routes to gpt-4o-mini and you see:
403 rate_limit_exceeded is refused before any model is chosen, so there is no source to advance to — for that, the token bucket above is the answer.
Per-environment keys — and per-environment workspaces
Don’t share one key across dev/staging/prod. Reasons:- Leaked dev keys have lower blast radius if scoped to a low-limit preset
- Per-env analytics are clearer
Separate keys do not buy separate rate-limit budgets. The preset that sets the limit is attached to the key, but the sliding window that counts is per workspace — so a dev key hammering a loop consumes the same window prod is refused on. If dev traffic must not be able to starve prod, put them in different workspaces.
Daily token caps
Some plans cap total tokens per day in addition to requests per minute. Hitting the cap is also a403, with its own code, and the counter resets at UTC midnight:
code, not on the status — rate_limit_exceeded and daily_tokens_exceeded share the 403 and need opposite responses. There is nothing to wait out here within the day:
- Reduce request volume
- Switch to a cheaper model
- Top up to a higher plan
When you are still being refused
If you have waited out the window and still get403 rate_limit_exceeded:
- Look at Usage → By API key — is one key dominating?
- Check API Keys → preset — is the preset lower than you remember?
- Did you ship a loop without rate limiting? Look at request volume in the last hour.
- Open a ticket — sometimes it’s our problem and we want to know.