Rate limits
Two limits apply to every /v1 request, and both are set per account by
eCourtDate as part of your subscription:
| Limit | Scope | Window | Exhausted response |
|---|---|---|---|
| Requests per minute | Your account, across every key and every /v1 endpoint | Fixed 60-second windows | 429 rate_limit_exceeded |
| Tokens per day | Your account, across chat completions, conversation messages, and embeddings | The UTC calendar day | 429 insufficient_quota |
Both are per account, not per key: two keys on the same account share one
budget. Neither is consumed by requests rejected with 401 or 403, because
authentication and scope are checked first. Contact eCourtDate to change
either limit.
Requests per minute
Every /v1 request counts one, whatever the endpoint: a GET /v1/models
and a DELETE /v1/conversations/{conversationId} cost the same as a chat
completion. The window is a fixed wall-clock minute, not a sliding one, so
the budget refills at the top of each minute. Requests rejected with 429
count toward the window too; a tight retry loop only prolongs the outage.
RateLimit headers
Every authenticated /v1 response, success or error, reports where you
stand (a 401, 403, 413, routing error, a body that is not valid JSON,
or a malformed Content-Length header is decided before the limiter runs
and carries no RateLimit-* headers; the headers are also absent if the
limiter itself is unavailable, see below):
RateLimit-Limit: 60
RateLimit-Remaining: 57
RateLimit-Reset: 41
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed per minute for your account |
RateLimit-Remaining | Requests left in the current window; never negative, 0 on a 429 |
RateLimit-Reset | Seconds until the current window resets, or, on a daily-quota 429, until the quota resets |
Read them to pace a batch job: when RateLimit-Remaining reaches 0,
sleep RateLimit-Reset seconds instead of sending a request that will be
rejected. With the OpenAI SDKs, use the with_raw_response (Python) or
.withResponse() (Node) variants of a call to reach the headers.
Daily token quota
The token quota counts the input and output tokens of every chat completion
and conversation message, and the input tokens of every embeddings request,
as reported in each response's usage. The quota resets at midnight UTC.
Three properties matter for integrations:
- The check runs before the request, not after. A request that pushes usage over the quota still succeeds; the next request is rejected.
- Exhaustion blocks everything. Once the quota is reached, every
/v1operation returns429insufficient_quota, including listing models and documents, which consume no tokens. - Streaming counts the same. Token usage for a stream is metered when
the finish chunk is produced, even if your client disconnects before
[DONE]. A stream that fails before the finish chunk meters nothing. Requeststream_options.include_usageto see the numbers (Streaming).
The two 429 responses
Both carry type: rate_limit_error and a Retry-After header, and differ
in code:
Requests per minute exhausted, Retry-After is the seconds left in the
current minute (1 to 60):
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 23
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 23
X-Request-ID: 4c2a9d1e7b3f4a6c8e0d1f2a3b4c5d6e
{
"error": {
"message": "Rate limit exceeded: 60 requests per minute.",
"type": "rate_limit_error",
"param": null,
"code": "rate_limit_exceeded"
}
}
Daily quota exhausted, Retry-After is the seconds until midnight UTC,
and RateLimit-Reset reports the same number rather than the minute
window:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 28417
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 28417
X-Request-ID: 9e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b
{
"error": {
"message": "Daily token quota exceeded: 200000 tokens per day.",
"type": "rate_limit_error",
"param": null,
"code": "insufficient_quota"
}
}
The same rate_limit_exceeded code is also used when the underlying
language model is itself rate limited (Retry-After: 10) and when the rate
limiter is briefly unavailable (message Rate limiter unavailable.,
Retry-After: 5); the message says which. The limiter-unavailable response
is the one 429 without RateLimit-* headers, because nothing was
computed. In every case Retry-After is the authoritative wait.
Retry-After
Retry-After is an integer number of seconds and is present on every 429
and on 503 upstream_unavailable
(5). Treat it as a floor: wait at least that long, then retry. For
rate_limit_exceeded the value never exceeds 60; for insufficient_quota
it can be most of a day, which is a signal to stop retrying and alert
instead.
Inside a stream, a rate limit that hits after the response has started arrives as an in-band error frame with no header (errors in a stream). Wait 10 seconds before resending.
Backoff guidance
- On
429, readRetry-Afterand wait that long. - If the same request is rejected again, back off exponentially on top of
Retry-After(for example 1, 2, 4, 8 seconds) with random jitter so concurrent workers do not retry in lockstep. - Cap the attempts (five is plenty) and surface the error after that.
- On
insufficient_quota, do not loop: schedule the work for after the reset or alert an operator. - Pace proactively using
RateLimit-RemainingandRateLimit-Reset.
The official OpenAI SDKs already do steps 1 to 3 for 429, 500, and
503 (two retries by default, honoring Retry-After); raise max_retries
for batch work. A hand-written loop for any HTTP client:
import random
import time
import openai
def create_with_backoff(client, max_attempts=5, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return client.chat.completions.create(**kwargs)
except openai.RateLimitError as e:
if e.code == "insufficient_quota":
raise # the quota resets on a known schedule; do not loop
retry_after = int(e.response.headers.get("retry-after", "1"))
if attempt == max_attempts:
raise
time.sleep(retry_after + random.uniform(0, 2 ** attempt))
import OpenAI from "openai";
async function createWithBackoff(client, params, maxAttempts = 5) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await client.chat.completions.create(params);
} catch (err) {
if (!(err instanceof OpenAI.RateLimitError)) throw err;
if (err.code === "insufficient_quota" || attempt === maxAttempts) throw err;
const retryAfter = Number(err.headers?.get("retry-after") ?? 1);
const jitter = Math.random() * 2 ** attempt;
await new Promise((r) => setTimeout(r, (retryAfter + jitter) * 1000));
}
}
}
Both SDKs retry on their own before these loops see an error; set
max_retries: 0 on the client if you want the loop to be the only retry
layer.
Errors
| Status | Code | When |
|---|---|---|
429 | rate_limit_exceeded | Requests per minute exhausted, the model is rate limited, or the limiter is unavailable; Retry-After set |
429 | insufficient_quota | Daily token quota reached; Retry-After is the seconds until the UTC reset |
503 | upstream_unavailable | Not a rate limit, but also carries Retry-After: 5 and deserves the same backoff |