# Throttling and rate-limit headers

Two things are throttled, for two reasons: The abuse-prone routes (sign-in, sign-in link, registration, password reset, and the round trips of provider sign-in) each carry a limit of their own, and **every call with an API key** also counts against a rate limit that belongs to the key. This page lists the headers, both limits, and the right reaction to `429`.

> **Type:** reference
> **For:** Anyone who builds a sign-in flow or has to interpret a `429`.
> **Prerequisite:** none

## The headers

Every response to which a limit applies carries the state of the window:

| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | requests allowed in the window in total |
| `X-RateLimit-Remaining` | of those, how many are still open |
| `X-RateLimit-Reset` | the point in time at which the window starts anew, in Unix seconds |
| `Retry-After` | seconds until the next permitted attempt, only when the limit is exceeded (`429`) |

All four are readable for browser clients on other origins, as are `X-Trace-Id`, `X-Correlation-Id`, and `Server-Timing`.

## The response when a limit is exceeded

```json
{ "code": "RateLimit.Exceeded", "message": "Too many requests", "traceId": "trc_…" }
```

The status is `429`, with `Retry-After`. A second code of the same area, `RateLimit.GlobalCircuitBreaker` (also `429`), indicates a platform-wide overload safeguard. The reaction is the same.

## The limits of the sign-in and registration routes

The limits apply per window and per counting key: “IP” counts per sender address, and “IP+email” counts per sender address and email address in the body.

| Route | Limit |
|---|---|
| `POST /v1/auth/identify` | 10 per minute (IP+email) and 40 per 10 minutes (IP) |
| `POST /v1/auth/login` | 5 per minute (IP+email) |
| `POST /v1/auth/login/choose-organization` | 10 per minute (IP) |
| `POST /v1/auth/mfa/verify`, `POST /v1/auth/mfa/recovery-code` | 5 per minute (IP) |
| `POST /v1/auth/login/magic-link` | 5 per minute (IP+email) |
| `POST /v1/auth/login/magic-link/consume` | 5 per minute (IP) |
| `POST /v1/auth/password-reset/request` | 5 per minute (IP+email) |
| `POST /v1/auth/password-reset/consume` | 5 per minute (IP) |
| `POST /v1/auth/onboarding/consume` | 5 per minute (IP) |
| `GET /v1/auth/login/sso/{providerId}` | 20 per minute (IP) |
| `GET\|POST /v1/auth/login/sso/callback` | 30 per minute (IP) |
| `GET /v1/auth/login/social` | 60 per minute (IP) |
| `GET /v1/auth/login/social/{provider}` | 20 per minute (IP) |
| `GET\|POST /v1/auth/login/social/callback` | 30 per minute (IP) |
| `POST /v1/auth/login/exchange` | 10 per minute (IP) |
| `POST /v1/auth/sso-recovery/consume` | 5 per minute (IP) |
| `GET /v1/auth/signup` | 60 per minute (IP) |
| `POST /v1/auth/signup/start` | 3 per 10 minutes (IP+email) and 10 per 24 hours (IP) |
| `POST /v1/auth/signup/verify`, `POST /v1/auth/signup/social` | 10 per minute (IP) |
| `POST /v1/auth/signup/complete`, `POST /v1/auth/signup/join` | 5 per minute (IP) |
| `POST /v1/tenants/me/sso/{providerId}/domains/{domain}/verify` | 10 per minute (IP) |

Further public and session-bound routes have windows of their own, for example invitation acceptance (`GET|POST /v1/invitations/public/{token}` and `…/accept`, each per IP), the analytics export (`POST /v1/analytics/metrics/{metric}/export`, per organization), and the VAT ID check (`POST /v1/me/billing-profile/vat-id/verify`, per organization). The description of the operation in the reference names the value where it names one, and otherwise the response headers show it.

## The rate limit per key

A call with an `ak_…` key counts against a rate limit that belongs to the key, on **every** route, including the read routes without a limit of their own. The reason is the nature of an automated client: A person clicks, a script queries in a loop, and the cheap read routes are what knock a stack over fastest.

| What | Value |
|---|---|
| Default | a value of the environment in the form `<count>/<window>`, such as `600/1m`. The window is given in `s`, `m`, or `h` |
| Per key | can be overridden at creation (field `rateLimit` in `POST /v1/me/api-keys`, where `null` means “the default of the environment”), and a rotation carries the value over |
| Counted | per key, not per sender address and not per organization |
| Headers | the same four as above, on every response |
| When exceeded | `429 RateLimit.Exceeded` with `Retry-After` |

Calls from a signed-in session, devices, renderers, and operator keys are not affected: They have limits of their own. If several rules apply to a call, for example the limit of the sign-in route and the rate limit of the key, the headers show the tightest one, and the rule that was exceeded first determines the error code.

What a client does with this: Track `X-RateLimit-Remaining` and slow your own loop before zero is reached. An import that creates ten thousand media items finishes faster with a small pause per call than one that runs into the wall and waits.

## The right reaction

| Rule | Why |
|---|---|
| Wait for `Retry-After`, then retry exactly once | Every attempt before that counts toward the window and extends the lockout. |
| Watch `X-RateLimit-Remaining` and slow down before zero | A client that knows the window never sees a `429`. |
| Do not hammer | Retries every second fill the window instead of emptying it. |

When a call gets `429`, the command line exits with code `8` (`rate-limited`), see [JSON output and exit codes](/cli/output-and-exit-codes).

## Related

- [The error format](/api/error-format)
- [JSON output and exit codes](/cli/output-and-exit-codes)
