Rate Limits
Rate limits protect the platform and ensure fair usage. SovereignEG enforces one request-rate limit per API key: RPM (requests per minute), on every model. On Student and Developer plans, requests on the plan's free-allowance models also count against the plan's own RPM, one budget for the whole organization, and are limited in how many may run at once (see Kinds of 429). Other models are not affected by either.
Spending is governed by your organization's EGP credit balance plus
the per-model sell rate — not by a separate tokens-per-minute cap.
When you run out of spendable credit on a priced model the API returns
402 quota_exceeded: at 0 EGP for organizations that have never made a
paid top-up, or at the small overdraft (shown on your dashboard) for
organizations that have; the next top-up settles a negative balance first. Before that point,
a chat completion your balance can't fully cover is served with
max_tokens lowered to what you can afford (X-Output-Clamped header),
and a request whose prompt you can't pay for gets 402 insufficient_balance
(see Error Codes), streaming requests (stream: true) included.
When the credit is only held by your organization's own requests still
running, a chat completion naming its model gets 429 inflight_busy
instead of these 402s, with stream: false too (see Kinds of
429): retry it. A model: "auto" request can get either:
the 402 when what the running requests leave is too little for its
classification step or for every model in its auto pool, the 429 when the
chosen model's gate refuses it. Embeddings, images and decision models keep
the 402; its message counts the credit held by requests still running
(with paying from the balance on, daily_allowance_exhausted says the
allowance is used up instead), and a retry once they finish can succeed.
On a model that reasons out of sight (Claude, OpenAI GPT-5 and o-series, Gemini thinking models, Grok) a streaming
request's max_tokens is never lowered below 1,024 output tokens: when your
credit covers fewer than that, it gets 402 insufficient_balance, or
429 inflight_busy after waiting up to about 25 seconds for them while your organization's other
requests still running hold it (see Kinds of 429). A stream that runs out of
credit part-way is not cut off: it ends cleanly (see
Streaming).
Monthly spend caps on an API key or a project are hard limits on the same
terms: a request a cap can only partly pay for is served with max_tokens
lowered to what the cap has left (X-Output-Clamped), a stream that
reaches a cap ends cleanly with stop_reason: "spend_cap_reached", and
you are never billed past a cap you set. A non-streaming request without
max_tokens is lowered to at most half of what a cap has left, so a second
request on the same key can run beside it; a max_tokens you send is held
in full until the request ends.
Limits by tier
Each plan tier has a requests-per-minute (RPM) ceiling:
Student and Developer plans have their own limits on the plan's free-allowance models, shown with each pack on the dashboard's Billing → Plans page. They never lower your per-key RPM: on every model it stays your tier's, from the table above.
Your effective limit is reported on every response from the inference
endpoints once your key is recognised, errors included, via the
X-RateLimit-Limit-Requests header (see Response headers).
Higher limits
The table above shows tier defaults. Your organization may have a custom ceiling that applies to every API key you own.
If you need a higher limit, contact us with your expected peak RPM and a one-line reason. Most increases are turned around the same business day.
Per-key and per-project narrowing
Owners can narrow further than the org default:
- Per-project RPM override — set in Dashboard → Projects → Limits. Useful for isolating an internal experiment from production traffic.
- Per-key RPM limit — set on the Create API key screen. Useful when you mint a key for a CI job or partner integration.
The API applies the strictest value across tier, org, project, and key. The result is reported on the response headers below.
How it works
SovereignEG uses a sliding window for RPM:
- Counts requests in a rolling 60-second window.
- Limits are enforced per API key. If your org has multiple keys, each key gets its own bucket — the per-key limits do not pool.
- On Student and Developer plans, each key keeps its own bucket at your account's normal limit, on every model. Requests on the plan's free-allowance models also count against the plan's RPM, one bucket for the whole organization: all its keys together can't go over it, so creating more keys doesn't raise it. Requests on other models never count against it.
Response headers
Every response from the inference endpoints reports your rate limit once your key is recognised, errors included:
X-RateLimit-Limit-Requests: 60
X-RateLimit-Remaining-Requests: 58
On the free-allowance models of a Student or Developer plan, the headers report whichever bucket, the key's or the organization's, has fewer requests left, so pacing on them keeps you clear of both. On every other model they report the key's.
- A request refused before it counts, such as a body that fails validation
(
422), an expired key (401) or a key or organization that may not use the endpoint (403), reports your key's bucket and spends nothing from it. - A
429from an RPM limit reports the bucket that refused the request, withX-RateLimit-Remaining-Requests: 0, next toRetry-After. - A request from an IP address that is not on your key's allow-list gets
no rate-limit headers, whatever the error (its
403 ip_not_allowed, or a422for its body). - No rate-limit headers come with a missing, unknown or revoked key's
401, a body the size limits refuse before your key is checked (413 request_too_large,503 large_request_backpressure), or a response our edge load balancer sends itself (its429,413,502or504). - On an error the headers are best effort: read them when present, and don't fail when they are missing.
When you hit the limit
You receive HTTP 429 with a Retry-After header:
{
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded: 60 requests per minute. Retries within the cooldown window will continue to be rejected.",
"code": "rate_limit_exceeded"
}
}Kinds of 429
A client can see these 429 responses; all use the JSON shape above, and
all should be handled the same way — honour Retry-After. Tell
them apart by the code, the Retry-After value and the message text:
- Per-key RPM limit (the platform).
Retry-Afteris 1–60 seconds: the time until the oldest request counted in your 60-second window expires. Rejected attempts are counted too, so a retry sent the momentRetry-Afterelapses can be refused again — back off further after each consecutive429. The message quotes your effective limit — the lowest of your tier, organization, project and key settings. This is the one you tune your client around. - Plan RPM on free-allowance models (Student and Developer plans). The
same
rate_limit_exceededcode andRetry-Afteras the per-key limit; the message quotes the plan's limit and says it countsacross your organisation. Only the plan's free-allowance models are refused: other models are not affected. - Edge limit (our load balancer).
Retry-After: 1and the message readsToo many requests at the edge; retry after 1 second.It applies above 100 requests per second or 400 concurrent requests on one key, above 200 requests per second or 800 concurrent requests from one IP address, above 20 requests per second per IP on other routes such asGET /v1/models,/v1/usageand/v1/credits, or when the platform's concurrent-request ceiling is reached. It signals a burst, not a quota: a rate burst clears within a second, and a concurrency rejection clears as soon as some of your in-flight requests finish. - Free-allowance concurrency (Student and Developer plans). Requests
on the plan's free-allowance models are limited in how many may run at
once:
concurrency_limit_exceededwhen your plan's limit is reached (Retry-After: 2; the message quotes the limit), orallowance_capacity_busywhen the free allowance's shared capacity is full (Retry-After: 5). Both clear as soon as a request finishes; other models are not affected. - Credit held by your own requests still running (
inflight_busy). On chat completions.Retry-After: 10and the message readsToo many requests in flight for this organization. Retry shortly.Your organization's spendable credit is held by its own requests still running, and this request would fit once some of them finish. A streaming request first waits for them, up to about 25 seconds (one wait per request, the key and project spend caps included), and gets this only if they are still running then. A non-streaming request gets it at once or after waiting up to about 10 seconds for them. Retry it: the credit comes back as they end. It counts across the whole organization, whichever key sent the request. Credit that is missing even with nothing running gets a402instead. Embeddings, images and decision models answer the402in this case too; its message counts the credit held by requests still running (with paying from the balance on,daily_allowance_exhaustedsays the allowance is used up instead), and a retry once they finish can succeed. Amodel: "auto"request can get either: the402when what the running requests leave is too little for its classification step or for every model in its auto pool, the429when the chosen model's gate refuses it.
Request size
Rate limits bound how often you may call; request size bounds how much one call may carry:
| Route | Maximum request body |
|---|---|
POST /v1/chat/completions, POST /v1/embeddings, POST /v1/decisions, POST /v1/systemone | 32 MiB (33,554,432 bytes) |
| Every other route | 10 MiB (10,485,760 bytes) |
The limit is on the raw JSON body, so base64 attachments count at their
encoded (4/3) size. Over it you get 413 request_too_large with the limit
quoted in the message — it is not retryable; drop earlier turns or
tool outputs, or shrink attachments. See
Error Codes. Inside the body, one chat
message holds at most 2,000,000 characters of text and 5 MB (decoded) of
media (over either: 422), and a request may carry at most 20 image or
video parts (over that: 400 too_many_image_parts).
A burst of very large requests can briefly exceed the platform's in-flight
body budget; those are refused with 503 large_request_backpressure and
Retry-After: 2, which the official SDKs retry automatically.
Handling rate limits
Python
import time
from openai import RateLimitError
def call_with_retry(client, **kwargs):
max_retries = 3
for attempt in range(max_retries):
try:
return client.chat.completions.create(**kwargs)
except RateLimitError as e:
retry_after = int(e.response.headers.get("Retry-After", 60))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
raise Exception("Max retries exceeded")Node.js
async function callWithRetry(client, params, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await client.chat.completions.create(params);
} catch (error) {
if (error.status === 429) {
const retryAfter = parseInt(error.headers?.['retry-after'] || '60');
await new Promise(r => setTimeout(r, retryAfter * 1000));
} else throw error;
}
}
}Best practices
- Implement exponential backoff — don't hammer the API on 429.
- Monitor your headers — check remaining quota before making requests.
- Batch when possible — one request with a longer prompt uses fewer RPM than many short requests.
- Use streaming — streaming counts as one request regardless of output length.
- Cache responses — if the same prompt is sent often, cache the result.