Skip to main content

Rate limits

Two limits apply to every /v1 request, and both are set per account by eCourtDate as part of your subscription:

LimitScopeWindowExhausted response
Requests per minuteYour account, across every key and every /v1 endpointFixed 60-second windows429 rate_limit_exceeded
Tokens per dayYour account, across chat completions, conversation messages, and embeddingsThe UTC calendar day429 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
HeaderMeaning
RateLimit-LimitRequests allowed per minute for your account
RateLimit-RemainingRequests left in the current window; never negative, 0 on a 429
RateLimit-ResetSeconds 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 /v1 operation returns 429 insufficient_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. Request stream_options.include_usage to 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

  1. On 429, read Retry-After and wait that long.
  2. 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.
  3. Cap the attempts (five is plenty) and surface the error after that.
  4. On insufficient_quota, do not loop: schedule the work for after the reset or alert an operator.
  5. Pace proactively using RateLimit-Remaining and RateLimit-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

StatusCodeWhen
429rate_limit_exceededRequests per minute exhausted, the model is rate limited, or the limiter is unavailable; Retry-After set
429insufficient_quotaDaily token quota reached; Retry-After is the seconds until the UTC reset
503upstream_unavailableNot a rate limit, but also carries Retry-After: 5 and deserves the same backoff