# AtmoRouter — Rate limits

Source: https://atmorouter.dev/docs/rate-limits

Only endpoints that intentionally differ from the standard API carry a chip; the chip links to the note that explains the difference.

Every budget below renews on a fixed window and answers `429` when spent. Find your number in the table, check whether that `429` carries a `Retry-After` header, and react accordingly. A `402` is never a rate limit — it means your balance is empty, so top up instead of retrying.

`/v1/chat/completions` `/api/dashboard/playground/chat` `/api/dashboard/login`

## Your budgets at a glance

| Surface | Limit | Scope | When exceeded |
| --- | --- | --- | --- |
| Inference (OpenAI + Anthropic) | Your key's rate_limit per minute (default 500; explicit 1..1000, anything else 400) | Per account — all keys combined | 429, no Retry-After; back off or ask support to raise it |
| Playground chat (dashboard session) | 20 requests/min | Per signed-in user | 429, no Retry-After |
| Concurrent requests at low balance | Up to $0.05: 1 in flight; up to $0.25: 3; above $0.25: unbounded | Per account | 429, no Retry-After; let requests finish or top up |
| Free-model fair-use (atmo/* promo) | 100M lifetime tokens per account; $1 lifetime deposits to unlock (both admin-configurable) | Per account | 429 naming the remedy; paid models unaffected |
| Sign-in + password step-up | 20 tries per IP + 8 per account per 15 min; 15-min lockout | Per IP and per account | 429 with Retry-After; wait the stated seconds |
| Signup email sends | 3 per email/hour; 20 per IP/hour; 90 global/day; 2700 global/month; 1-min resend cooldown (defaults) | Per email / IP / global | 429 code rate_limited with Retry-After + retry_after_seconds |
| Signup completion | 30 per IP/hour; 120 global/hour (defaults) | Per IP / global | Same 429 shape as email sends |
| Provider webhooks | 30/min per IP on every callback | Per client IP | 429 with Retry-After |

Your inference budget belongs to your account, not to any single key: every request counts against the owning user's per-minute window, so minting extra keys never multiplies it. Omit `rate_limit` when creating a key and it resolves to 500; an explicit value must be between `1` and `1000` — anything else (including `0` and negatives) is rejected with `400`. The catalogue read (`GET /v1/models`) carries no per-minute budget.

## What a 429 looks like

Inference refusals quote your account's own limit back at you. On the OpenAI surface:

```json
{
  "error": {
    "message": "rate limit exceeded: 500 requests/min for your account (all keys combined) — wait a minute or contact support to raise it",
    "type": "rate_limit_exceeded"
  }
}
```

The Anthropic surface carries the same message in its own envelope:

```json
{
  "type": "error",
  "error": {
    "type": "rate_limit_error",
    "message": "rate limit exceeded: 500 requests/min for your account (all keys combined) — wait a minute or contact support to raise it"
  }
}
```

Playground refusals are shorter (`rate limit exceeded` with type `rate_limit_exceeded`), and tired-balance refusals say `too many concurrent requests for current balance — wait for in-flight requests to complete or top up`. Free-model refusals carry the promo message with type `rate_limit_exceeded` and code `free_tier_quota_exceeded` on the OpenAI surface (`rate_limit_error` on Anthropic). Signup refusals use code `rate_limited`.

| Who answers 429 | Retry-After? | Body signal |
| --- | --- | --- |
| Inference, playground, low-balance concurrency, free-model quota, upstream passthrough | No — back off exponentially | OpenAI envelope (Anthropic envelope on /v1/messages) |
| Sign-in, password step-up, account-deletion confirm | Yes — wait the stated seconds | {"error": "..."} string plus the header |
| Signup register / resend / complete | Yes, plus retry_after_seconds in the body | {"error": {"code": "rate_limited", ...}} |
| Provider webhooks (sumopod, epusdt) | Yes — wait the stated seconds | {"error": "too many webhook calls"} |

We send no `X-RateLimit-*` headers. The `429` itself — plus `Retry-After` where the table above says so — is the only rate signal.

## Staying under

Honor `Retry-After` wherever it is present; otherwise back off exponentially — the full retry pattern lives under [Errors](https://atmorouter.dev/docs/errors) and is not repeated here. Keep concurrent requests serialized while your balance is thin, or top up past $0.25 to lift the concurrency cap entirely. Paid models are never gated by the free-model quota: a free-model `429` names its own remedy in the message.

Retry only what is transient. Retry `429`, `502` and `503` with exponential backoff. `402` means your balance is empty, so top up instead of retrying. Never retry `400`, `401`, `403` or `404`.

Inference, playground, sign-in and webhook budgets are counted in memory per gateway process: a restart clears them, and replicas do not share them. Signup email quotas live in the database and survive restarts. Signup quota defaults shown here are operator-configurable via environment.

## Where these numbers come from

These are the live gateway defaults; if one changes, this page changes with it.

| Number |
| --- |
| Sliding 1-min window; legacy limits ≤ 0 fall back to the 500 default |
| 500 default on omit; explicit 1..1000, anything else → 400 |
| Per-user enforcement (all keys combined) |
| Playground 20/min |
| Concurrency 1 / 3 / unbounded |
| Sign-in 20 + 8, 15-min window + lockout |
| Retry-After senders (auth, signup, webhooks) |
| Signup quotas + 429 shape |
| Free-model quota + refusal shape |
| Upstream 429 passthrough |
