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

# Rate Limits & Errors

> Rate limiting, plan quotas, and which API errors are worth retrying.

Two different things stop a request: **rate limits**, which are transient and you should
retry, and **plan quotas**, which are not transient and you must not retry. Telling them
apart is the whole skill.

<Info>
  For the per-plan numbers behind these quotas, see [Plans](/api-reference/ai-cms/plans).
  For the full list of status codes, see [Status codes](/api-reference/ai-cms/status-codes).
</Info>

***

## Rate limits

Requests are limited to **50 requests per second per IP**. Exceeding it returns `429`.

```json theme={null}
{
  "success": false,
  "statusCode": 429,
  "message": "Too many requests",
  "data": { "hint": "Rate limit exceeded. Retry after a short delay." }
}
```

<Warning>
  The limit is per IP, not per key. If your backend fans out across instances behind a
  shared egress IP, they all share one budget of 50 requests per second.
</Warning>

### Backing off

Exponential backoff with jitter. A fixed delay from many workers at once just queues up
again immediately.

```js theme={null}
async function withRetry(fn, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    const res = await fn();
    if (res.statusCode !== 429) return res;

    const wait = Math.min(2 ** i * 250, 8000) + Math.random() * 250;
    await new Promise((r) => setTimeout(r, wait));
  }
  throw new Error("Rate limited after 5 attempts");
}
```

<Note>
  Where the SDK exposes response headers, prefer the server's <code>retry-after</code>.
  The JavaScript SDK surfaces it as
  <code>res.error.headers\["retry-after"]</code>; the others do not expose headers, so
  use the computed backoff there.
</Note>

***

## Plan quotas

When you exhaust a quota on your plan, the API returns `426 Plan limit exceeded` and
`data` reports exactly how far over you are.

```json theme={null}
{
  "success": false,
  "statusCode": 426,
  "message": "ai_tokens limit exceeded (used: 10020, limit: 10000)",
  "data": { "used": 10020, "limit": 10000, "remaining": 0, "overflow": 20 }
}
```

<Warning>
  <strong>Never retry a <code>426</code>.</strong> Retrying cannot help — the quota
  only changes when you upgrade, the period rolls over, or you delete something. Putting
  it in the same retry loop as <code>429</code> just burns rate-limit budget.
</Warning>

| **Situation**             | **What to do**                                                              |
| ------------------------- | --------------------------------------------------------------------------- |
| **Content quota reached** | Check usage in the dashboard, upgrade, or archive content to free capacity. |
| **AI credits exhausted**  | Same — the AI endpoints draw on a separate token pool.                      |
| **Hit mid-migration**     | Stop the batch job and resume from where it stopped once usage is reduced.  |

<Note>
  AI token quota is checked with a <strong>projection</strong>, not just current usage:
  the API estimates what your request will cost and rejects it if
  <em>used + estimated</em> would exceed the limit. A request that would have fit can
  still fail if it is large. Check
  <a href="/api-reference/ai-cms/plans">Plans</a> for what each plan includes.
</Note>

***

## Which errors are worth retrying

| **Code** | **Retry?** | **What to do**                                                      |
| -------- | ---------- | ------------------------------------------------------------------- |
| **400**  | No         | The request is malformed. Fix the input.                            |
| **401**  | No         | The key is missing, invalid, expired, or disabled.                  |
| **403**  | No         | The role lacks permission. Use a different key.                     |
| **404**  | No         | The resource does not exist or is not visible to this role.         |
| **406**  | No         | Validation failed at the domain level — see the endpoint reference. |
| **409**  | No         | Something with that slug or ID already exists.                      |
| **422**  | No         | Schema validation failed. `data` names the field.                   |
| **423**  | No         | The workspace is inactive. Fix billing.                             |
| **426**  | **No**     | Plan quota exhausted. Back off for real — do not retry.             |
| **429**  | **Yes**    | Back off exponentially with jitter.                                 |
| **500**  | Maybe      | Retry once or twice, then surface it.                               |
| **502**  | **Yes**    | Upstream failure, usually temporary.                                |
| **503**  | **Yes**    | The service is unavailable. Back off and retry.                     |
| **504**  | **Yes**    | The upstream was just slow. Retry.                                  |

***

## Handling it in the SDKs

The SDKs return errors as values rather than raising, so you can branch without a
try/catch. Set the throw option if you would rather have exceptions.

```ts theme={null}
// JavaScript
const res = await cms.article.create(payload);

if (!res.ok) {
  if (res.error.status === 429) return scheduleRetry();
  if (res.error.status === 426) throw new Error("Quota reached — pausing the job");
  throw new Error(`${res.error.status}: ${res.error.message}`);
}
```

<Note>
  The Go SDK's <code>ThrowOnError</code> panics rather than returning an error, and the
  panic value is a plain string. Prefer checking <code>res.Ok</code> there.
</Note>

***

<CardGroup cols={2}>
  <Card title="Plans" icon="layers" href="/api-reference/ai-cms/plans" arrow="true">
    What each plan includes, limit by limit.
  </Card>

  <Card title="Status codes" icon="list" href="/api-reference/ai-cms/status-codes" arrow="true">
    Every code and its default message.
  </Card>
</CardGroup>
