> ## Documentation index
> Fetch the complete documentation index at: https://docs.predictefy.com/llms.txt
> Use it to discover every available page before exploring further.

# Rate limits & retries

> Per-plan request windows, what a 429 actually means, and how to back off correctly.

Every API key is rate limited per plan. Exceeding the window returns `429 RATE_LIMITED`
with `retryable: true` in the standard [error envelope](/quickstart/#4-the-error-envelope).

Rate limiting and [credits](/guides/credits/) are separate controls. The limiter caps how
_fast_ you may call; credits cap how _much_ you may call in total. A request can pass the
limiter and still fail with `402 INSUFFICIENT_CREDITS`, or fail the limiter without ever
being charged.

## Limits by plan

| Plan       | Requests     | API keys   | Concurrent WebSocket streams |
| ---------- | ------------ | ---------- | ---------------------------- |
| Free       | 60 / min     | 1          | 2                            |
| Builder    | 300 / min    | 3          | 20                           |
| Pro        | 3,000 / min  | 10         | 100                          |
| Scale      | 10,000 / min | 25         | 500                          |
| Enterprise | negotiated   | negotiated | negotiated                   |

The window is a **rolling 60 seconds per API key**, not per account and not per endpoint.
Two keys on one account each get the full allowance; one key spread across ten processes
shares a single allowance.

Enterprise plans and individual keys can carry a bespoke override. An override is applied
as a **per-second** window rather than per-minute — so a key provisioned at 50/s is
allowed 50 in any given second, not 3,000 spread freely across a minute.

## What the response tells you

```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "rate limit exceeded — retry shortly",
    "retryable": true
  }
}
```

:::caution[There is no `Retry-After` header]
The API does not currently return `Retry-After`, `X-RateLimit-Remaining`, or any other
rate-limit header. Do not write a client that waits on one: branch on the `code` field and
apply your own backoff. This is a current gap, stated rather than omitted.
:::

## Backing off

With no server-supplied delay, use exponential backoff with jitter. The window is 60
seconds, so a client retrying every second spends its next allowance on failures.

```ts
async function withRetry<T>(call: () => Promise<T>, attempts = 5): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await call();
    } catch (err) {
      const code = (err as { code?: string }).code;
      // Only these are worth retrying. A 400 or 401 will fail identically forever.
      if (code !== 'RATE_LIMITED' && code !== 'PLATFORM_UNAVAILABLE') throw err;
      if (attempt >= attempts - 1) throw err;
      // 1s, 2s, 4s, 8s … plus jitter so parallel workers do not resynchronise.
      const backoff = 2 ** attempt * 1000 + Math.random() * 1000;
      await new Promise((r) => setTimeout(r, backoff));
    }
  }
}
```

The [TypeScript SDK](/guides/sdk/) retries `GET` requests once on 429 by default
(`retryOn429`). **Writes are never auto-retried** — a submit or cancel that may have
reached the venue must not be replayed by a client library. For those, retry deliberately
and send an `Idempotency-Key`; see [Trading & execution](/guides/trading/).

## Which errors to retry

| Code                                          | HTTP      | Retry?                                                 |
| --------------------------------------------- | --------- | ------------------------------------------------------ |
| `RATE_LIMITED`                                | 429       | Yes — back off, then retry                             |
| `PLATFORM_UNAVAILABLE`                        | 503       | Yes — back off, then retry                             |
| `CATALOG_UNAVAILABLE` / `HISTORY_UNAVAILABLE` | 503       | Yes — back off, then retry                             |
| `INSUFFICIENT_CREDITS`                        | 402       | No — retrying cannot succeed until the balance changes |
| `VALIDATION_ERROR`                            | 400       | No — fix the request                                   |
| `UNAUTHORIZED`                                | 401       | No — fix the key                                       |
| `NOT_SUPPORTED`                               | 400 / 501 | No — an honest capability gap, not a failure           |

`retryable` is present on every error and is the field to branch on. Treat it as
authoritative over the HTTP status.

## Staying under the limit

- **Prefer cursors over parallel offset pages.** Following `nextCursor` keeps one request
  in flight; twenty parallel offset pages spend twenty of the allowance in one second.
- **Batch where a batch verb exists.** `fetchOrderBooks` takes many outcomes in one
  request. Note it is [priced by items](/guides/credits/), so it saves allowance rather
  than credits.
- **Stream instead of polling.** A [WebSocket subscription](/guides/streaming/) delivers
  book and trade updates without consuming the request window at all. Polling a book every
  second on Free spends the entire minute allowance on one market.
- **Cache what does not move.** Venue capability maps (`has`) and taxonomy
  (`fetchCategories`, `fetchTags`) change rarely; re-fetching them per request is pure
  overhead.
- **Spread scheduled work.** Offset cron jobs by a random delay so batch runs do not
  collide with each other or with interactive traffic.

## When rate limiting itself is degraded

If the limiter's backing store is unreachable, the API **fails open for reads** — requests
are served rather than rejected, and credits remain the spend backstop. Side-effecting
routes fail **closed** with `503 PLATFORM_UNAVAILABLE` instead, because replaying an
uncertain write is worse than refusing it.

No special handling is required. It is documented so that a burst of `503`s on writes
while reads continue is recognisable as designed behaviour rather than a partial outage.
