Rate limits
Limits are per key and per package, set by SP Cambo rather than by the upstream provider, and published before you buy — plus two service-wide ceilings that apply to every key.
Where your limits come from
Limits are attached to the package you bought and to the key you are calling with. They are published on the pricing page for every package and in the model catalogue for every alias, and repeated in your dashboard for the entitlements you actually hold.
This page does not restate the numbers. Quoting them here would go stale the moment a package is re-configured, and the catalogue is the authoritative source.
What is limited
| Limit | Field | Notes |
|---|---|---|
| Requests per minute | requests_per_minute | How many calls you may start in a rolling minute. The first limit most interactive sessions meet. |
| Tokens per minute | tokens_per_minute | Metered throughput. A few very large requests can hit this while your request count is low. |
| Concurrency | concurrency | How many requests may be in flight at once. Long streams hold a slot for their whole duration. |
| Maximum request size | max_request_bytes | Rejected before any upstream call, so an oversized request costs nothing. |
| Maximum output tokens | max_output_tokens | Caps a single completion. A request asking for more than this is refused up front. |
A null value means that dimension is not limited for that package or key. It does not mean zero, and it does not mean unlimited either — the service-wide ceilings below still apply.
Ceilings that apply to every key
Two protections sit in front of your own limits and are enforced whatever your package says, including when a dimension on your key is null:
- an admission ceiling of 120 requests per minute per API key on the inference endpoints, and 60 per minute on the two key and model read endpoints. It is checked before your key is even looked up, so it applies to invalid credentials too.
- a maximum request size for the service as a whole, applied in addition to your key's
max_request_bytes. The smaller of the two wins.
These exist to keep one client from degrading the service for everyone, not as a product tier. If you are hitting them, you are almost certainly better served by spreading load across several keys — which is also how you find out which workload is producing it.
When the limiter itself cannot be reached, requests are refused with rate_limiter_unavailable rather than admitted unchecked. That is deliberate: an unmetered request is worse than a refused one, for you as well as for us.
Reading a 429
A rate-limited request returns HTTP 429. There are two codes, and they are worth telling apart: rate_limit_exceeded means a per-minute ceiling on requests or tokens, and concurrency_limit_exceeded means too many requests are in flight at once. The first clears when the minute rolls over; the second clears as soon as one of your own requests finishes, which is why it is sent with a much shorter Retry-After.
Where the server can say how long to wait, it sends a Retry-After header. Prefer that value over your own timer — it reflects the actual window, and ignoring it makes the queue worse for you as well as everyone else.
Neither is a billing event. The request never reached a model, so nothing was metered and your balance is untouched.
Correct retry behaviour
Retry with exponential backoff and jitter, and cap the number of attempts. Jitter matters: a fleet of clients retrying on identical timers re-collides on every cycle.
const MAX_ATTEMPTS = 5
async function send(request) {
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
const res = await fetch(request)
if (res.status !== 429 && res.status < 500) return res
// Honour the server first; fall back to exponential backoff with jitter.
const retryAfter = Number(res.headers.get('retry-after'))
const wait = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: Math.min(2 ** attempt * 500, 20_000) + Math.random() * 250
await new Promise(resolve => setTimeout(resolve, wait))
}
throw new Error('Rate limited after retries')
} Retry 429 and 5xx. Do not retry 402, 401, 403 or 422 — a quota, credential or validation failure returns the same answer every time, and retrying it just consumes your request budget.
Long CLI sessions
Claude Code and Codex CLI already back off on 429s, so an occasional one during heavy use is normal and self-correcting. Sustained rate limiting usually means one of three things:
- several tools or machines are sharing a single key — give each its own;
- a script is looping without backoff alongside your interactive session;
- the package's limits are lower than the way you are working needs.
Per-key activity in your dashboard tells you which key is producing the load, which is the fastest way to tell these apart.
Concurrency and streaming
A streaming request holds a concurrency slot from the reservation until the stream ends. Parallel agents that each open a long stream can exhaust concurrency while the requests-per-minute figure still looks comfortable, which reads as unexplained 429s. Bound your own parallelism rather than letting a worker pool grow to whatever the machine allows.
Limits that are not ours
Upstream capacity can also apply back-pressure. When that happens you get a retryable error rather than a silent failure or a partial charge, and the reservation is released. Your own SP Cambo limits are unaffected by it.
See errors for the full code list and which of them are worth retrying.