# Rate limits & spend caps

RouterPlus checks these limits before a request reaches a provider:

- the key's requests-per-minute (RPM) limit
- the organization's token and concurrency limits
- the provider pool's capacity
- guardrail budgets and key spend limits
- the monthly spend caps
- the balance

Each limit refuses with a typed error. The error names its class in the body and in
`x-tm-error-code`. RouterPlus does not slow down or queue a request without telling you. A
refused request costs nothing.

- [Errors](/docs/errors) lists the error shapes.
- [Limits and capacity](/docs/admission) covers pools, fairness and outages.

In the console, open Billing > Rate limits.

## Per-key requests per minute

Every key has an RPM limit over a rolling 60-second window. All gateway instances count in one
shared store. The count for a request can stay in the window for up to 61 seconds.

| Key | RPM |
|---|---|
| First trial key shown after verified browser sign-in | 20 |
| Key created in the console or with the identity API | 300 |
| The managed Playground key of the console | 60 |

### Trial rate

- Until the organization buys credit, every customer key and the organization as a whole run at
  20 requests per minute. Credit that RouterPlus granted counts as bought credit. More keys do
  not add more requests.
- The first purchase removes the trial rate within seconds. No key changes.
- The trial rate does not apply to requests served only by your own provider keys (BYOK). It
  does not apply to the managed Playground key.
- Past card-verification bonuses are promotional credit. They do not count as purchases.

An organization that joined through a RouterPlus signup invitation (for example, at a
hackathon) counts as paid from the start. By default, the organization and each key get these
limits: 600 requests per minute, 10,000,000 tokens per minute and 32 requests in flight.

`GET /v1/limits` shows the values that apply to your key.

RouterPlus can also limit an organization on trial credit when it detects unusual usage. The
request gets `429 usage_limited` (with `retry-after`) or `403 usage_restricted`. The message asks
you to contact support. Support can review the account. A purchase usually removes the limit.

### The rate-limit refusal

Above the limit, RouterPlus refuses the request before any reservation or provider work:

```
HTTP/1.1 429 Too Many Requests
retry-after: 23
x-tm-error-code: rate_limit
x-tm-error-origin: gateway_admission
x-tm-limit-scope: key
x-tm-limit-kind: rpm
x-tm-limit-id: key:<key id>
```

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "configured quota exhausted",
    "type": "requests",
    "metadata": {
      "error_type": "rate_limit",
      "origin": "gateway_admission",
      "limit_scope": "key",
      "limit_kind": "rpm",
      "limit_id": "key:<key id>",
      "retryable": true,
      "retry_at": "2026-09-24T10:14:04.000Z"
    }
  },
  "request_id": "..."
}
```

| Surface | `code` | `type` |
|---|---|---|
| `/v1/chat/completions`, `/v1/responses` | `rate_limit_exceeded`, the OpenAI code that OpenAI SDKs and coding agents read | `requests`, or `tokens` for a token limit |
| `/v1/decisions`, images, video | `rate_limit` | `invalid_request_error` |
| Anthropic surface | A native `rate_limit_error` with the same `error_type` and `metadata`, so the Anthropic SDK's backoff runs | |

`error_type` and `x-tm-error-code` are always `rate_limit`.

### `retry-after`

- A rate limit's `retry-after` is in whole seconds, 1 to 59. The `usage_limited` refusal
  sends 60 or 3,600.
- On an RPM or token limit, it is the time until the rolling window has room for the request.
- On a limit on requests at once, it is 10 seconds. A slot becomes free when one of your running
  requests ends. No window can predict that time.
- `metadata.retry_at` is the same instant.
- Other requests can take the room first. Then a retry can get another 429 with the rest of the
  wait. Wait for `retry-after`, then retry.

> [!NOTE]
> You cannot raise a key's RPM ceiling yourself. On [`/console/limits`](https://demo.routerplus.com/console/limits) you can only lower a key's RPM and set its other limits.

### Provider rate limits

A provider can rate-limit RouterPlus upstream. Then RouterPlus fails over to another deployment
and pauses that provider's pool. You see a provider `rate_limit`
(`x-tm-error-origin: upstream`) only when every candidate was rate-limited. Its `retry-after` is
the provider's value, up to 59 seconds. A provider 429 that names no wait gets 10 seconds.

## Organization tokens and concurrency

Every organization and every key also has a tokens-per-minute limit and a concurrency limit, in
the same shared window:

| Scope | RPM | Tokens per minute | Concurrent requests |
|---|---|---|---|
| Organization (all keys together) | 300 (20 until the first purchase) | 1,000,000 | 8 |
| Key | its ceiling above | 1,000,000 | 8 |

- These are the defaults. Set your own on [`/console/limits`](https://demo.routerplus.com/console/limits)
  or with `POST /api/admission-limits`.
- A key's RPM cannot go above its ceiling. You can add a limit for one model on top.
- Under a contract, RouterPlus can replace these defaults and the keys' RPM ceiling for
  your organization. A limit that you set still applies and can only lower them.
- At admission, tokens are an estimate: one token for each four UTF-8 bytes of the request,
  rounded up, plus a media allowance, plus the output limit (at most 32,768 tokens of it). When
  the request completes, observed usage replaces the estimate. The balance hold in
  [The worst-case reserve](#the-worst-case-reserve) counts one token for each byte.
- A refusal has the 429 shape above, with `limit_scope` `org` or `key` and `limit_kind` `tpm` or
  `concurrency`. On the OpenAI surface, a token-limit refusal has `"type": "tokens"`. A refusal
  for requests at once sends `retry-after: 10`.

Every admitted response has `x-tm-remaining-rpm` and `x-tm-remaining-tpm`. Each is the smallest
remaining amount across the scopes that the request claimed, at that moment.
`GET /v1/limits?model=<id>` on the gateway returns the effective limits for your key.

### Provider pools

Each house deployment has a declared pool. One organization can use at most half of a pool's usable
capacity, or three quarters on the decision-model pools. When the pool is full, you
get a 429 with `x-tm-error-origin: upstream_quota` and `x-tm-limit-scope: pool`.
[Limits and capacity](/docs/admission) covers workspace and principal scopes.

### Dedicated endpoints

A [dedicated endpoint](/docs/dedicated-endpoints) has its own limits. For its traffic, they
replace the defaults above.

- The limits can include a requests-per-second limit: a count per second, or a
  [paced limit](/docs/dedicated-endpoints#the-paced-limit) that holds a short burst in a queue.
- A paced request that waited has `x-tm-queue-ms`, the wait in milliseconds.
- Above the hard rate, the refusal is `x-tm-limit-kind: rps`. When the wait would pass its
  bound, it is `rps_queue`.
- Refusals have `x-tm-limit-scope: endpoint`.

## Budgets, key limits and grants

A guardrail budget limits the spend of its targets for a day, week, month or lifetime. A key's own
spend limit (`limit` and `limit_reset` on the key) limits one key. A used-up budget or
key limit returns `403 budget_exceeded`. To see every budget and key limit with the spend of
this period, open Governance > Budgets in the console.

A grant raises a budget or a key limit for a set time. Then the base value applies again. To
request, approve, reject or cancel a grant, open Access > Grants in the console. The
[management API](/docs/api-management#grants) has the same actions.

Granted limits show in these places:

- `x-tm-grants` on the response: the ids of the active grants that applied to the request. A
  refusal also has it.
- The `budget_exceeded` message. It adds "(raised by grant <id>)" when a grant raised the limit.
- `GET /v1/guardrails/:id/status`: `limit_usd` is the granted limit, `base_limit_usd` the
  guardrail's own limit, and `grant_id` the grant.
- Key reads (`GET /v1/keys`, `GET /v1/keys/:id`) and `GET /v1/key`: `limit` is the granted
  limit, `base_limit` the key's own limit, and `grant_id` the grant.
- Budget alerts. They count against the granted limit.

A guardrail with `"overridable": false` takes no grant. On the org, its budgets limit every
grant.

## Monthly spend caps

You can set two optional hard caps in the console:

- An **org-wide** cap on every key, in Billing > Credits
  ([https://demo.routerplus.com/console/billing](https://demo.routerplus.com/console/billing)).
- A **per-key** cap on one key, with **Set cap** in Access > API keys
  ([https://demo.routerplus.com/console/keys](https://demo.routerplus.com/console/keys)).

Rules:

- Both caps run on the **UTC calendar month** and reset at the first instant of the next month.
- They can overlap. The strictest one wins.
- In the console, a blank cap field changes nothing. A cap of 0 removes the cap.
- The console marks keys near their cap. It shows each key's month-to-date spend against
  its cap.

### How enforcement works

Before RouterPlus sends anything, it reserves a worst-case cost for the request. See
[The worst-case reserve](#the-worst-case-reserve). The cap check adds this month's settled
spend and the worst-case cost of every request in flight, this one included. If the sum is
above the cap, RouterPlus refuses the request before it costs anything. RouterPlus claims the
in-flight amount synchronously, so concurrent requests cannot pass the cap together.

The check is conservative. RouterPlus can refuse a request near the cap even when its settled
cost would have fit. A tight `max_tokens` lowers the worst-case reserve and gives you more room
near the cap.

### The cap refusal

A cap breach is the standard out-of-credits 429: `insufficient_quota`, a class that OpenAI SDKs
recognize. The message and a header give the reset time:

```
HTTP/1.1 429 Too Many Requests
x-tm-error-code: insufficient_quota
x-tm-cap-reset: 2026-10-01T00:00:00.000Z
x-should-retry: false
```

```json
{
  "error": {
    "code": "insufficient_quota",
    "type": "insufficient_quota",
    "message": "org monthly cap exceeded; resets 2026-10-01T00:00:00.000Z",
    "metadata": { "error_type": "insufficient_quota", "origin": "gateway_admission", "retryable": false }
  },
  "request_id": "..."
}
```

- The message names the scope of the cap that was reached: `org` or `key`.
- On the Anthropic surface, the body is a native `billing_error` with the same `error_type` and
  message.
- The refusal has no `retry-after`. `x-should-retry: false` tells the OpenAI and Anthropic SDKs
  and Claude Code not to retry.
- Do not retry before the reset time. Backoff does not change a hard cap.

## The worst-case reserve

The cap and balance checks use a pessimistic cost estimate. There is one exception: a request
without an output limit holds at most 32,768 output tokens. A longer answer is still charged in
full.

| Input | Value |
|---|---|
| Estimated input tokens | One per UTF-8 byte of the request body, plus 65,536 per image or document part. An image sent inline as base64 counts the 65,536 only, not its bytes. |
| Assumed output tokens | Your `max_tokens` (or `max_completion_tokens`), all of it. When omitted, at most 32,768, and the request is sent with the model's own maximum, fitted to its context window. More than the model's own `max_output_tokens` (32,768 when no maximum is declared) is a 400. |
| Images (`/v1/images/generations`) | Assumed output = `n` × the model's per-image ceiling. |
| Rounding | The reserve rounds **up**. Settlement rounds **down**. |

The reserve exists only while the request is in flight. At settlement, RouterPlus releases it and
records the observed cost: the usage that the provider reports, at the prices pinned when the
request was admitted. RouterPlus debits your balance and caps only for observed usage, with two
exceptions:

- An attempt can end before the provider reports usage, because you cancelled it or the
  provider failed during the answer. It is charged its input and the output already streamed to
  you.
- An attempt whose outcome the gateway could not observe (`unknown_after_crash`) settles at the
  reserved maximum, because the provider may have billed the call. See
  [Pricing & billing](/docs/pricing).

## Balance exhaustion

The org balance must cover the worst-case reserve of every request that the org has in flight,
this one included. When it cannot, the refusal is the same `insufficient_quota` 429, with a
different message:

```json
{
  "error": {
    "code": "insufficient_quota",
    "type": "insufficient_quota",
    "message": "insufficient marketplace credits for authorized house fallback",
    "metadata": { "error_type": "insufficient_quota", "origin": "gateway_admission", "retryable": false }
  },
  "request_id": "..."
}
```

- To tell the two `insufficient_quota` cases apart in code: only cap breaches have
  `x-tm-cap-reset`.
- A balance refusal has no `retry-after` and no `x-should-retry`. When your own requests in
  flight hold the balance, the balance becomes free as they end, and a retry can succeed.
  Otherwise, add credits.

> [!TIP]
> Signup alone does not add free credit. An invitation link from RouterPlus does. Buy credits in [Billing](https://demo.routerplus.com/console/billing). Eligible purchases may receive matching promotional credit. If calls return `insufficient_quota`, check your balance and spend caps there.

## Handling the two 429s

Both refusals are 429s, so the OpenAI SDK raises `RateLimitError` for both. Read `error_type`.
Retry a `rate_limit` after `retry-after`. Stop on `insufficient_quota`.

```python
import os, time
from openai import OpenAI, RateLimitError

client = OpenAI(base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"])

def create(**kw):
    try:
        return client.chat.completions.create(**kw)
    except RateLimitError as e:
        kind = e.response.json()["error"]["metadata"]["error_type"]
        if kind == "rate_limit":
            time.sleep(int(e.response.headers.get("retry-after", "10")))
            return client.chat.completions.create(**kw)  # one retry
        raise  # insufficient_quota: retrying cannot help; add credits or raise the cap
```

```typescript
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://demo-api.routerplus.com/v1", apiKey: process.env.TM_API_KEY });

try {
  await client.chat.completions.create({
    model: "claude-haiku-5-5",
    messages: [{ role: "user", content: "hello" }],
  });
} catch (err) {
  if (err instanceof OpenAI.APIError && err.status === 429) {
    const kind = (err.error as { metadata?: { error_type?: string } })?.metadata?.error_type;
    if (kind === "rate_limit") {
      // wait Number(err.headers?.get("retry-after")) seconds, then retry
    } else {
      // insufficient_quota: stop; x-tm-cap-reset (cap breaches only) says when a cap reopens
      const capReset = err.headers?.get("x-tm-cap-reset");
    }
  }
}
```

## Other limits

- **Monthly spend caps only.** The monthly caps use the UTC calendar month and nothing shorter.
  For a daily or weekly limit, use a guardrail budget or a key limit with `limit_reset`.
- **No self-serve RPM raises.** Trial keys are 20 RPM. Console keys are 300, or 20 until the
  organization's first purchase. A limit that you set can only lower a key's RPM.
- **RouterPlus refuses sign-ups that look automated or abusive.** The page says that unusual
  activity was detected. If this happens to you, contact support. Existing accounts sign in
  without limit.
- **Request size:** a body over 10 MB gets a 413 `request_too_large`.
- **Image response size:** an upstream image response over 32 MiB is a 502 `gateway_error` and
  bills nothing. It is not a 413. Lower `n` or the quality.
- **An image request holds one admission slot for the whole render**, typically 10 to 60 s.
  The gateway sends no first byte before the image is ready.
