How rate limits work
This page explains the requests-per-minute and tokens-per-minute figures the platform shows for your plan, and the 429 the gateway returns when either is crossed.
What it is
Two throughput limits, checked in path, in process, before the provider is contacted: requests per minute and tokens per minute. They are about throughput, never about money. Crossing one throttles a request that is then never metered and never billed; crossing a spend ceiling is a different mechanism with a different code, on the budgets page.
The published tiers, sustained per minute:
| Plan | Requests per minute | Tokens per minute |
|---|---|---|
| Free | 100 | 200,000 |
| Developer | 600 | 1,000,000 |
| Team | 600 | 1,000,000 |
| Growth | 1,200 | 2,000,000 |
| Scale | 3,000 | 5,000,000 |
| Enterprise | set in your agreement | set in your agreement |
Every bucket holds 2x its per-minute figure, so a burst of 2x the sustained rate is admitted at once and then refills at the sustained rate. Agent fan-out is normal traffic, not abuse.
These figures are indicative. Rate limits are indicative, never a guarantee: the limiter runs inside each gateway process, so the throughput you actually get can land above or below the published figure. More than one process serves production traffic, so a workspace spread evenly across them is admitted at up to twice the published rate, while uneven balancing can throttle it nearer half. We publish the number so you can size against it, not as a floor we have committed to hold, and there is no service level attached to it.
Developer and Team are deliberately identical: throughput was never the axis the ladder prices on, bands and features are. Enterprise publishes no figure at all: those limits are contract terms, and the limits endpoint returns null with a note rather than a guess. The table lives in one config struct with one entry per tier, matched exhaustively, so adding a plan fails to compile rather than silently inheriting somebody else's ceiling.
How it is computed
Four buckets per request. Your key's requests, your key's tokens, your workspace's requests, your workspace's tokens. The key buckets stop one noisy key from consuming the whole allowance; the workspace buckets bind across all of a workspace's keys, so two keys each running at half the limit exhaust it together.
The bucket. Each is a token bucket refilled continuously, not a window that resets on the minute. Capacity and refill are separate knobs: capacity is twice the per-minute figure, deliverable at once, and refill is the per-minute figure spread evenly across the minute, in integer arithmetic. Catch-up is capped at two windows, matching the burst, so a bucket idle for an hour comes back exactly full and never banks credit past the ceiling. A clock that moves backwards is a refill no-op, never free capacity.
The order. Key requests, key tokens, workspace requests, workspace tokens, in that order. If any stage refuses, every stage already debited is credited back, so a throttled request consumes nothing anywhere, it does not eat the allowance it just failed to fit into.
The token estimate. Nothing knows a request's token count before it runs, so the tokens-per-minute stage debits an estimate: the declared Content-Length divided by 4, never less than one token. A request that declares no length, a chunked upload for instance, estimates at that floor of one token. After the response the estimate is replaced by the provider's reported input plus output tokens: an over-estimate is credited back, an under-estimate is debited forward, so sustained throughput converges on the real limit. Requests per minute never reconciles, a request is a request. A wildly large reported figure is bounded at twice the bucket's capacity, so one bad reading delays the next requests rather than wedging the bucket for hours.
The refusal. Everything below is the wire contract; branch on code, never on the status alone.
| On a throttle | Value |
|---|---|
| HTTP status | 429 |
error.message | Rate limit reached for this workspace. |
error.type | rate_limit_error |
error.code | rate_limit_exceeded |
error.param | null |
Retry-After | whole seconds, at least 1, at most 60 |
x-recovea-request-id | the id of the refused request |
The envelope is OpenAI-shaped, so an SDK that already understands provider 429s understands this one. A budget cap in pause mode also answers 429, but carries the code budget_exceeded; that is money, not throughput.
Retry-After. Not a fixed backoff: the deficit, how far over capacity this request would put the bucket, divided by the refill rate, rounded up to whole seconds and clamped into 1 to 60. It is the wait until the refused request would fit.
Worked example
A Free workspace, one key, sending small requests inside one minute. The requests bucket holds 200: twice the sustained 100 a minute, because capacity carries the burst while refill does not. From a full bucket the first 200 are admitted; the 201st finds no room and is refused with the body above. Refill runs at 100 a minute, so about 1.2 seconds later the bucket has recovered two requests' worth, exactly two more are admitted, and the next is refused again.
Now the token dimension on the same tier: 200,000 sustained, so a bucket holding 400,000. One request estimated at 300,000 tokens is admitted, leaving 100,000. A second request estimated at 150,000 does not fit and is refused, but its requests-per-minute debit is rolled back, so it costs nothing. When the provider reports the first request actually used 50,000 tokens, the reconcile credits 250,000 back, and the 150,000-token request then fits.
What it does not include
- Money. A rate limit never refuses on spend, and a budget never refuses on throughput.
- Billing. A throttled request is not metered, carries no receipt, and appears in no export: it never reached the provider.
- Provider-side limits. Your provider enforces its own limits on your own key; a provider 429 is relayed to you as the provider sent it, including the provider's own
Retry-After. - Concurrency, request-size and monthly-quota limits. Only the two dimensions above are enforced.
- The dashboard's own sign-in throttle, which is a separate control on a separate plane.
Limits and current status
- Buckets are per gateway node. Each node holds its own counters in memory, and nothing shares rate counters between nodes the way spend counters are published and merged. The published figure is what one node enforces, so the effective ceiling across nodes is higher when your traffic spreads evenly and lower when it does not: a workspace whose requests land mostly on one node can be throttled nearer half the published figure. That is exactly why the figure is published as indicative rather than as a floor.
- A request larger than the whole bucket can never be admitted. A single call whose token estimate exceeds the tier's bucket capacity, twice its per-minute figure, does not fit in an empty bucket either. It still receives a
Retry-Aftercomputed from the deficit, and waiting it out will not help: only a smaller request or a larger tier will. That is a sharp edge we have not smoothed. - The estimate can be far off for a chunked request. A body with no declared length is admitted on the one-token floor, however large it turns out to be, and is corrected only after the response. A burst of such requests can pass the token limit before the reconciles land.
- The plan on the hot path rides a snapshot. The tier is stamped from the entitlement snapshot the control plane publishes about every five seconds. A workspace whose entitlement push has not yet arrived admits at the Free tier, the most conservative published tier, so a new or changed plan settles within seconds rather than at the instant you click.
- The limiter fails open. If its own state faults, the request passes through rather than being refused: availability wins, and the missed limit is counted internally. Refusing on our own fault is not a trade we make.
- The values are code defaults. They live in a config struct, one entry per tier, so they can move to a parameter store without a code change. Nothing sets them per workspace today: a workspace is admitted at its plan's tier and nothing else.
- Budgets. Only workspace-scoped caps reach the gateway. Key- and project-scoped caps can be created and displayed, but nothing refuses on them.
- Counterfactual savings are measured, never applied. No lever is activated in any deployed environment.
- The Recovea Score engine is not shipped. No workspace has a score today.
Where to check us
- Read your own tier:
GET /platform/v1/limitsreturnsrequestsPerMin,tokensPerMin,tier, and a link back to this page. A contact plan returns null for both figures with a note, never an invented number. - Burst against the gateway with one key on the Free tier and count the admissions before the first refusal. From an idle bucket, against one node, you should see 200 admitted and the 201st refused: the burst is twice the sustained 100 a minute. Against production, where more than one node serves traffic, expect the count to land above that.
- Check that the throttle cost you nothing: immediately after a refusal, the allowance you had left is unchanged. Every bucket the refused request touched was credited back.
- Check that it was not billed: search the request feed and the ledger export for that minute. A throttled request has no row anywhere. A 429 with a matching metered row is a bug.
- Check
Retry-Afteragainst the arithmetic above, then retry after that many seconds. The same request should fit, unless its own estimate is larger than the bucket.
If traffic is refused that you believe was inside your limit, send us the x-recovea-request-id header from the 429. A limit that refuses traffic it should have admitted is a bug, not a support question.