> ## Documentation Index
> Fetch the complete documentation index at: https://docs.laces.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits

> Per-client limits, the 429 response and Retry-After.

Limits are counted **per API client**, not per IP address, so many servers
behind one address are not throttled as a crowd.

| Route group | Limit |
| - | - |
| [`POST /oauth/token`](/api-reference/v1/oauth/token) | About **10 per minute** per client id and IP, plus a ceiling of 60 per minute per IP |
| Reads: events, order reads, session read | About **300 per minute** per client |
| Writes: create order, pay, create session | About **60 per minute** per client |
| Any request made with a publishable key | The client limit above, **plus** about 120 per minute per IP |

## When you hit a limit

You get `429 Too Many Requests` with a `Retry-After` header: the number of
whole seconds until the window resets. Wait at least that long before
retrying.

The body depends on the route.

<CodeGroup>
  ```json API routes theme={null}
  {
    "code": 429,
    "message": "Too many requests, please slow down",
    "error": "rate_limited",
    "error_code": "rate_limited"
  }
  ```

  ```json Token endpoint theme={null}
  {
    "error": "temporarily_unavailable",
    "error_description": "Too many token requests. Retry after the number of seconds in the Retry-After header."
  }
  ```
</CodeGroup>

On API routes the 429 carries `error_code: "rate_limited"`, like every other
error. Responses also carry the draft `RateLimit-Limit`, `RateLimit-Remaining`
and `RateLimit-Reset` headers.

The token endpoint uses the OAuth error shape. RFC 6749 has no dedicated
throttling error, so it uses `temporarily_unavailable`, which OAuth libraries
treat as retryable.

Browser code can read `Retry-After` too: the Partner API exposes it (and
`X-Request-Id`) to cross-origin JavaScript, so `res.headers.get("Retry-After")`
works for publishable-key calls.

<Note>
  Limits are **counted per Laces server instance**, and the API runs on as
  many instances as traffic needs. So the numbers are approximate: under load
  you may get somewhat more than the limit before a `429`, and the
  `RateLimit-*` headers describe only the instance that answered. Treat the
  numbers as a ceiling to stay well under, not a budget to spend.
</Note>

## Staying well under the limits

* **Cache the access token** for its full hour. Minting a token per request
  is the most common way to hit the token limit. See
  [Authentication](/authentication#caching-the-token).
* **Cache event reads** for a minute or two. Events change rarely.
* Prefer **webhooks** to polling orders. If you must poll a pending mobile
  money payment, poll every few seconds and stop once the order leaves
  `payment_initiated` / `prompt_sent`.

```js theme={null}
async function withRetry(fn, attempts = 4) {
  for (let i = 0; ; i++) {
    const res = await fn();
    if (res.status !== 429 || i === attempts - 1) return res;
    const wait = Number(res.headers.get("Retry-After") ?? 1);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.