# Rate limits and concurrency

Two account-level limits set by plan: requests per minute, shared across every product, and concurrent requests, counted per product. How each limit counts, how to read the x-ratelimit headers, and how to retry a 429.

Two limits shape traffic, and the plan sets both. Requests per minute are counted once for the whole account: one window covers every product and every key. Concurrent requests are counted per product: speech, transcription, OCR, and translation each get their own pool of the plan's size, and [Document Extraction](https://paxalabs.com/docs/extraction) draws on the OCR pool. The catalog, [account](https://paxalabs.com/docs/api/me), and [health](https://paxalabs.com/docs/api/health) endpoints are never limited, and [feedback](https://paxalabs.com/docs/api/feedback) has daily caps of its own. Each plan's numbers are on the [pricing page](https://paxalabs.com/pricing). Today they are:

| Plan | Requests per minute | Concurrent requests |
| --- | --- | --- |
| Free | 10 | 1 |
| Starter | 60 | 5 |
| Standard | 200 | 15 |
| Pro | No limit | 60 |

Concurrent requests apply per product: the Standard plan can hold 15 speech requests, 15 transcription requests, 15 OCR requests, and 15 translation requests in flight at the same time. [GET /v1/me](https://paxalabs.com/docs/api/me) reports both numbers for the authenticated account.

## One request window, separate concurrency pools

The two limits count differently, on purpose:

- Requests per minute are shared. One window covers the whole account: every product, every key, and every [live synthesis leg](https://paxalabs.com/docs/live-streaming) together.
- Concurrent requests are separate. Speech, transcription, OCR, and translation each get their own pool of the plan's size, and document extraction requests count in the OCR pool. A slow translation batch never blocks speech, document reading, or transcription, and the reverse.
- An open [live connection](https://paxalabs.com/docs/api/live) holds one speech slot for as long as it stays connected, whether or not it is synthesizing right now.

## The rate limit window

The limit is a sliding 60-second window. The previous minute still counts by its unelapsed fraction. A burst cannot double up at a minute boundary, and capacity frees up continuously.

## Read the headers

On plans with a limit, every synthesis response reports the budget: `x-ratelimit-limit`, `x-ratelimit-remaining`, and `x-ratelimit-reset` (Unix time in seconds). Plans without a limit send no rate limit headers.

Response headers:

```http
HTTP/1.1 200 OK
Content-Type: audio/mpeg
x-request-id: 7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a
x-credits-charged: 0.86
x-ratelimit-limit: 60
x-ratelimit-remaining: 42
x-ratelimit-reset: 1785762360
```

## Concurrent requests

A slot is held by one in-flight request. An HTTP synthesis holds it from admission until its response finishes, a translation or document reading until its JSON returns, and a live connection from upgrade to close. When the account holds every slot for a product, new requests to that product answer [429 concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited) and nothing is charged.

A `concurrency_limited` response carries no `retry-after` header, because the wait ends when one of your own requests finishes. Retry after one completes, or close a live connection you no longer need. Note the `x-ratelimit-remaining` value in the sample. The request window still has room, and the 429 is about in-flight slots.

429 response:

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
x-ratelimit-limit: 60
x-ratelimit-remaining: 41
x-ratelimit-reset: 1785762420

{"title": "concurrency_limited", "status": 429}
```

## Stay under the limits

Both limits count requests, and the size of each request does not matter. Batch work into fewer calls. One [translation request](https://paxalabs.com/docs/api/translate) carrying an array of segments counts once against the window and holds one slot. One TTS request carrying a paragraph counts once, where per-sentence calls count once each.

Keys share the account's limits. Adding keys does not add capacity. Pace retries with `retry-after`. A [live WebSocket](https://paxalabs.com/docs/api/live) upgrade can be refused with 429 like any synthesis request, under either limit.

## Handle 429 responses

Two codes answer 429, and `title` tells them apart. [rate_limited](https://paxalabs.com/docs/errors#rate_limited) means the minute window is exhausted. Honor the `retry-after` header. [concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited) means every slot for the product is held. Retry once one of your requests finishes. A simple wait-and-retry wrapper covers both for most workloads.

429 response:

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
retry-after: 12
x-ratelimit-limit: 60
x-ratelimit-remaining: 0
x-ratelimit-reset: 1785762420

{"title": "rate_limited", "status": 429}
```

TypeScript:

```typescript
async function withRetry(request: () => Promise<Response>): Promise<Response> {
  for (;;) {
    const response = await request();
    if (response.status !== 429) return response;
    // rate_limited sends retry-after. concurrency_limited does not,
    // and a short wait covers a slot freeing up.
    const seconds = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}
```
