Rate Limits
Rate limits protect providers and your budget. They are evaluated atomically in one step before any provider is contacted, so a rate-limited request never costs you anything.
Scopes
| Scope | Configured by | Limits |
|---|---|---|
| API key | You, per key, in the dashboard | rate_limit_rps, rate_limit_rpm, tokens_per_minute, concurrent_requests |
| Organization | Operator (quotas) | Requests / tokens per window across all keys |
| Model | Operator | Per-model caps to protect scarce capacity |
| Global | Operator | Platform-wide ceilings |
All applicable scopes are checked; the first one exceeded produces the error. Windows are sliding (not fixed calendar minutes), so bursts at a boundary do not double your allowance.
tokens_per_minute is checked against the estimated token count (ceil(characters / 4) for messages, plus ~1,000 per image, plus max_tokens or the model default for output) since the real count is only known afterwards.
The 429 response
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 2
X-Request-Id: req_01J6ZK3M9PQR7S8T9V
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit exceeded for api_key (60 requests/minute)",
"request_id": "req_01J6ZK3M9PQR7S8T9V",
"retry_after": 2
}
}code | Trigger |
|---|---|
rate_limited | Requests per second / per minute at any scope. |
tokens_per_minute_exceeded | Estimated tokens for the current minute exceed the key's tokens_per_minute. |
concurrency_exceeded | More than concurrent_requests in flight. Wait for one to complete. |
provider_rate_limited | All upstream providers returned 429 (after fallback). Different cause, same handling. |
Headers
| Header | Notes |
|---|---|
Retry-After | Seconds to wait (integer). Present on every 429. The same value is in error.retry_after. |
X-Request-Id | Always present. |
The gateway does not currently emit X-RateLimit-Limit / X-RateLimit-Remaining style headers. Use Retry-After and the per-key usage shown in the dashboard to size your concurrency.
How to back off
- On
429, sleep forRetry-Afterseconds (fall back to exponential backoff starting at 1 s with jitter if the header is missing), then retry the identical request. - Cap total retries (3–5) and surface the error to the caller afterwards.
- Reduce concurrency when you see repeated
concurrency_exceeded; a semaphore sized to the key'sconcurrent_requestsprevents them entirely. - For throughput work, spread load across time rather than across keys — organization and model limits apply regardless of how many keys you create.
async function withBackoff<T>(fn: () => Promise<Response>, max = 4): Promise<Response> {
for (let attempt = 0; ; attempt++) {
const res = await fn();
if (res.status !== 429 || attempt >= max) return res;
const ra = Number(res.headers.get('retry-after'));
const delay = Number.isFinite(ra) && ra > 0 ? ra * 1000 : Math.min(8000, 1000 * 2 ** attempt) * (0.5 + Math.random());
await new Promise((r) => setTimeout(r, delay));
}
}The official openai SDK already retries 429 with backoff (maxRetries, default 2) and honours Retry-After.
import random, time, requests
def with_backoff(send, max_attempts=4):
for attempt in range(max_attempts + 1):
res = send()
if res.status_code != 429 or attempt == max_attempts:
return res
ra = res.headers.get("Retry-After")
delay = float(ra) if ra else min(8, 2 ** attempt) * (0.5 + random.random())
time.sleep(delay)The official openai Python SDK retries 429 automatically (max_retries, default 2).
Rate limits vs. spend limits
A 429 means slow down; a 402 means you cannot afford this request right now (balance, key budget or organization spending limit). Backing off does not fix a 402 — see API Keys for the difference.