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 429 from an RPM limit reports the bucket that refused the request, with X-RateLimit-Remaining-Requests: 0, next to Retry-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 a 422 for 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 (its 429, 413, 502 or 504).
  • 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-After is 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 moment Retry-After elapses can be refused again — back off further after each consecutive 429. 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_exceeded code and Retry-After as the per-key limit; the message quotes the plan's limit and says it counts across your organisation. Only the plan's free-allowance models are refused: other models are not affected.
  • Edge limit (our load balancer). Retry-After: 1 and the message reads Too 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 as GET /v1/models, /v1/usage and /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_exceeded when your plan's limit is reached (Retry-After: 2; the message quotes the limit), or allowance_capacity_busy when 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: 10 and the message reads Too 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 a 402 instead. Embeddings, images and decision models answer the 402 in this case too; 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. 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.

Request size

Rate limits bound how often you may call; request size bounds how much one call may carry:

RouteMaximum request body
POST /v1/chat/completions, POST /v1/embeddings, POST /v1/decisions, POST /v1/systemone32 MiB (33,554,432 bytes)
Every other route10 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

  1. Implement exponential backoff — don't hammer the API on 429.
  2. Monitor your headers — check remaining quota before making requests.
  3. Batch when possible — one request with a longer prompt uses fewer RPM than many short requests.
  4. Use streaming — streaming counts as one request regardless of output length.
  5. Cache responses — if the same prompt is sent often, cache the result.
Rate Limits | SovereignEG Docs