# Rate & spend limits

> INFRO gateway rate limits, provider account quotas, estimated-spend budgets, subscription access, and how to handle 429 and 402 errors.

Source: https://infro.io/docs/api/limits

> **Note:** INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See [Early access](https://infro.io/early-access).

Every INFRO key is governed by two independent limits: a **rate limit** on requests per minute and an optional **spend limit** capping the dollars it can consume. Rate limits protect throughput; spend limits protect your budget. Both are enforced per key, so you can tune them per service in the [console](https://dash.infro.io).

This page covers how each limit works, the headers that report your standing, and what the `429` and `402` failure modes look like. For the full error catalog and retry rules, see [Errors](https://infro.io/docs/api/errors).

## Rate limits

Each key gets its own requests-per-minute ceiling, chosen when you create the key from fixed tiers: **60**, **100**, **200** (the default), **300**, **500**, **1,000**, **2,000**, **5,000**, **10,000**, **20,000** or **50,000 RPM**. The console also takes a custom number and shows the tier it rounds down to before the key is created. It is a property of the key rather than of your subscription — a plan sets your monthly request allowance, not your per-minute rate — so different keys in one organization can carry different ceilings. Every response reports the ceiling actually in force in `X-RateLimit-Limit`, so read your live limit from there rather than assuming the base.

Your own provider accounts retain their model, token and request quotas. INFRO does not add a shared pool of purchased capacity. Failover can use another eligible connection you configured; retries count once toward the INFRO request allowance, while provider-side usage follows each provider's own billing rules.

## Rate limit headers

Every response — success or error — carries three headers describing the key's current window.

| Header | Example | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | `200` | Requests allowed per minute for this key. |
| `X-RateLimit-Remaining` | `173` | Best-effort upper bound on requests left in the current window — see the note below. |
| `X-RateLimit-Reset` | `1755950520` | Unix timestamp (seconds) at which the current window resets. Exact. |

```text
HTTP/2 200
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 173
X-RateLimit-Reset: 1755950520
```

**`X-RateLimit-Reset` is exact. `X-RateLimit-Remaining` is not, and the difference matters.** Rate limiting is enforced at the Cloudflare location serving your request rather than from one global counter, so `Remaining` counts only what that location has seen. It is therefore an *upper bound* on what is left: your key may have spent more than it says, never less. Back off as it approaches zero — that is what it is for — but do not schedule against it, and do not read a non-zero value as a guarantee that the next request is admitted. `Reset` is a property of the window rather than of any counter, so it is exact. The authoritative signal is still the `429` itself and its `Retry-After`.

## When you hit the rate limit

Exceeding the RPM budget returns `429` with type `rate_limit_exceeded` and a `Retry-After` header giving the seconds to wait.

```text
HTTP/2 429
Retry-After: 60
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1755950520

{
  "error": {
    "message": "Rate limit exceeded: 200 requests per minute for this key.",
    "type": "rate_limit_exceeded",
    "code": null
  }
}
```

Honor `Retry-After` when present, and retry with exponential backoff plus jitter. `429` is one of the four retryable statuses, alongside `408`, `502`, and `503`; never auto-retry `400`, `401`, `402`, or `404`. Reference retry implementations are in [Errors](https://infro.io/docs/api/errors).

## Spend limits

A key's spend limit is a hard USD ceiling on the estimated provider cost of its requests, set when you create the key in the [console](https://dash.infro.io) or changed later under **Edit spend limit**. Once the key has used its limit, every subsequent request fails with `402 budget_exceeded` until the limit resets or you raise it. The same `402` is returned by an organization, project or member budget; the message names which one refused.

Each limit resets on a schedule you choose. **Daily** resets at midnight UTC, **weekly** at midnight UTC on Monday, and **monthly** at midnight UTC on your organization's budget cycle day. **Never** is a lifetime cap: it does not reset, so it bounds what a leaked key can spend in total. Every window is anchored to UTC, not to a time zone, so the limit resets at the same instant for everyone on your team.

Usage is measured in estimated provider cost. A request to a model with no price cannot be measured, so by default it is refused (`402`, code `unpriced_model_under_budget`) while a blocking limit applies, unless you enter a rate for the model under Prices or allow unpriced traffic under Budgets in the console.

```json
{
  "error": {
    "message": "Budget exceeded: this API key has used its weekly spend limit. Requests are refused until the limit resets at midnight UTC on Monday, or until you raise it under Keys in the console.",
    "type": "budget_exceeded",
    "code": "key_budget"
  }
}
```

A key's daily, weekly or monthly limit returns `key_budget`. A lifetime (`never`) cap returns `key_lifetime_limit` instead, because nothing but raising it will restore access.

> **Warning:** A `402` is not transient. Retrying will not succeed until the budget that refused it is raised or removed — exclude it from automatic retry logic. Inspect `error.code`: `subscription_required` and `trial_payment_failed` mean software access must be restored. Budget-related failures require changing the named budget.

For per-request accounting, every response includes `usage.cost` — an estimate of your provider's charge — beside `provider` and `connection`; see [Chat completions](https://infro.io/docs/api/chat-completions). For a key's cumulative estimated spend, query `GET /v1/key`.

## Checking a key's usage

`GET /v1/key` returns the current key's label, spend limit, and usage against it, plus how often the limit resets and when it next does. Poll it from dashboards or pre-flight checks to see where a key stands.

**curl**

```bash
curl https://api.infro.io/v1/key \
  -H "Authorization: Bearer $INFRO_API_KEY"
```

**Python**

```python
import os
import requests

resp = requests.get(
    "https://api.infro.io/v1/key",
    headers={"Authorization": f"Bearer {os.environ['INFRO_API_KEY']}"},
)
print(resp.json())
```

**TypeScript**

```typescript
const resp = await fetch("https://api.infro.io/v1/key", {
  headers: { Authorization: `Bearer ${process.env.INFRO_API_KEY}` },
});
console.log(await resp.json());
```

```json
{
  "label": "prod-checkout",
  "limit": 250.00,
  "usage": 187.42,
  "limit_remaining": 62.58,
  "limit_period": "weekly",
  "limit_resets_at": "2026-09-28T00:00:00.000Z"
}
```

- `label` (string) — The name assigned to the key in the console. Useful for confirming which key a service is actually using — see [Authentication](https://infro.io/docs/authentication).
- `limit` (number | null) — The key's spend limit in USD. `null` if no limit is set. If a key carries both a resetting limit and a lifetime cap, this is whichever has less room left.
- `usage` (number) — USD spent by this key against `limit` — inside the current window for a daily, weekly or monthly limit, or over the key's lifetime for `never`. With no limit set, the key's lifetime total.
- `limit_remaining` (number | null) — `limit` minus `usage`, never below zero. `null` when the key has no spend limit.
- `limit_period` ("daily" | "weekly" | "monthly" | "never" | null) — How often `limit` resets. `never` is a lifetime cap. `null` when the key has no limit.
- `limit_resets_at` (string | null) — When the limit next resets, as an ISO 8601 timestamp in UTC. `null` for `never` and when the key has no limit.

## Handling burst traffic

- **Use one key per service.** Each key has its own RPM budget and spend ceiling, so a spike in one service cannot starve another — and per-key labels make cost attribution trivial.
- **Watch `X-RateLimit-Remaining`.** Throttle client-side as it approaches zero instead of absorbing `429`s. It is a best-effort per-location count, so treat it as a hint to slow down rather than an exact budget, and keep honouring `Retry-After` on the `429` itself.
- **Respect `Retry-After` and add jitter.** Synchronized retries from a fleet of workers re-trigger the limit in lockstep; randomized backoff breaks the herd.
- **Consolidate small calls.** The RPM budget counts requests, not tokens — a streamed response is still one request, so fewer, larger calls stretch the budget further.
- **Pick a higher tier.** The per-minute limit is set on the key, not by your plan, so moving up a plan does not raise it. If a service consistently runs near `X-RateLimit-Limit`, issue it a key on a higher tier (up to 50,000 RPM). The ceiling is per key, so splitting traffic across keys also spreads it. Remember that your provider accounts keep their own rate limits, which INFRO does not raise.

## Subscription and trial access

Serving requires an active software entitlement. `402 subscription_required` means the subscription or trial no longer grants access, and `402 trial_payment_failed` means the charge at the end of a trial failed and its grace period ended. Request allowances are soft on every plan, trials included: past the allowance requests are served and counted, never refused for volume, and do not create automatic overage charges. These conditions are distinct from `budget_exceeded`. See [Billing](https://infro.io/docs/org/billing).

### Response cache hits

A hit from the [response cache](https://infro.io/docs/features/caching) counts against the key's rate limit and toward the plan's monthly request allowance like any other request. It takes no spend-limit or budget reservation, and it is served even when a blocking limit is exhausted, because a hit is not sent to your provider and adds nothing to its bill.
