# Rate limits

Source: https://kirora.io/docs/api/reference/rate-limits

Stay within the per-key and per-workspace request limits, and back off correctly with Retry-After.

Rate limits cap how many requests a key may make in a window; past it, the API returns
429 `RATE_LIMITED`.

Calls draw on one of three budgets. **High-volume calls** have their own, larger one, so a
burst of receipts or a contact sync never starves the calls that manage your contacts and
campaigns, and the reverse. **Events** have a third, the largest, so page views never
spend what your sends need. Every other call shares the general budget.

| Budget | Routes |
| --- | --- |
| Events | `POST /v1/events/track`, `POST /v1/events/batch`, and the app SDKs' events |
| High volume | `POST /v1/transactional/messages` and `/batch`, `POST /v1/contacts/batch` |
| General | Every other `/v1` call, including `POST /v1/contacts/identify` |

A batch counts as one request per item: 100 contacts in `POST /v1/contacts/batch` cost
100 of the high-volume budget, so at the per-key default a sync runs at 3,000 contacts a
minute. Events count the same way, from a server or an app: a workspace records up to
60,000 events a minute by default.

| Limit | General | High volume | Events | Scope |
| --- | --- | --- | --- | --- |
| Per API key | 300 / minute | 3,000 / minute | (the workspace's) | One key |
| Per workspace | 1,200 / minute | 6,000 / minute | 60,000 / minute | All keys in the workspace combined; for events, your apps' events too |

A flood guard of 72,000 requests a minute per IP sits above both, so one server can call
for many workspaces. A request must pass every limit that applies to it.

Defaults are configurable per deployment, so treat these as the shape of the system rather
than guaranteed numbers. Do not hard-code them in a client; read `Retry-After` instead.

The per-workspace limit is the one that surprises integrators: splitting work across several
keys does not buy more throughput, because they share the workspace budget. Keys are
for separating *authority* and rotation, not for raising limits.

## Being limited

Exceeding a limit returns **429** with code `RATE_LIMITED`:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many API requests. Wait for the time in Retry-After before trying again.",
    "details": { "retryAfterSeconds": 12 }
  }
}
```

The delay appears twice on purpose. `Retry-After` is the conventional HTTP header that
proxies, HTTP clients and generic retry middleware already understand;
`details.retryAfterSeconds` is the same number where structured error handling can reach
it without parsing headers. **They always agree.** Prefer the header.

A 503 `SERVICE_UNAVAILABLE` from the rate limiter itself means the limiter could not be
reached, not that you were limited. Back off and retry.

## Seeing what is left

Every response given after the key's budget check, a 429 from that check included,
carries what is left of the budget it drew on, by the tighter limit (the key's or the
workspace's), for the current minute. The per-IP 429 and any refusal made before the
key check (a missing or bad key, for example) carry no `RateLimit-*` headers; the per-IP
429 still has `Retry-After`:

```http
RateLimit-Limit: 300
RateLimit-Remaining: 287
RateLimit-Reset: 41
```

`RateLimit-Reset` is seconds until the window starts over. Pace a batch job by
`RateLimit-Remaining` and you will rarely see a 429 at all.

## Backing off correctly

1. On 429, wait at least `Retry-After` seconds. Do not retry sooner — an early retry is
   refused and counts against the limit again.
2. Add jitter. Many workers that all woke on the same limit and all slept the same
   interval will collide again on the same second.
3. Cap total attempts and surface the failure rather than retrying forever.
4. Honour `Retry-After` on any status that sends it, not only 429: a 503 carries it when
   the wait is known, as when checking a sending domain while the email provider is busy
   (30 seconds). Otherwise apply exponential backoff on 5xx.

`Retry-After` is always whole seconds, never a date. A reasonable default: up to 3
retries, `Retry-After` when present, otherwise `2^attempt` seconds, each with ±25% jitter.

Only retry operations that are safe to repeat. All `GET` requests are safe. For a write,
see [Idempotency](https://kirora.io/docs/api/reference/idempotency) and the retry note on each operation's reference page:
some take an `Idempotency-Key`, some are safe because a
repeat sets the same values again, and some are not safe to repeat. A 429 means the
request was refused before it ran, so it can be sent again after the wait.

## Staying under the limit

- **Batch.** `POST /v1/lists/{id}/contacts` adds many contacts in one call. Adding them
  one per request is both slower and the usual cause of hitting a limit.
  `POST /v1/transactional/messages/batch` takes up to 100 messages, but counts as one request
  per message: it saves round trips, not budget. The same holds for `POST /v1/contacts/batch`.
- **Raise `limit` when paginating.** Reading 100 at a time costs a fifth of the requests
  that reading 20 at a time does.
- **Do not poll for completion.** Sends are asynchronous; subscribe to
  [webhooks](https://kirora.io/docs/api/webhooks) instead of asking repeatedly whether a campaign has finished.
- **Bound your concurrency.** One worker respecting the limit is better than twenty
  workers discovering it simultaneously.

## Request size

Body size limits per route are in [Limits](https://kirora.io/docs/api/limits#request-size).

## Next steps

- [Errors](https://kirora.io/docs/api/reference/errors) — every code and which to retry
- [Idempotency](https://kirora.io/docs/api/reference/idempotency) — make retries after a 429 safe
- [Webhooks](https://kirora.io/docs/api/webhooks) — learn outcomes without polling
- [Authentication](https://kirora.io/docs/api/reference/authentication) — keys and scopes