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

# Rate limits

> A per-key technical window, reported in headers on successful JSON responses and on 429.

Each key has its own window: **120 requests per 60 seconds** by default. It is an operational abuse ceiling, not a plan quota. Usage itself is metered by the owner's balance, not by this counter, and keys of the same owner do not share the window.

Authentication runs before the limiter, so an unauthenticated request never spends a key's budget.

## Headers

Successful JSON responses (`200`) and the `429` answer carry the state of the window:

| Header                  | Meaning                                    |
| ----------------------- | ------------------------------------------ |
| `X-RateLimit-Limit`     | Requests allowed per window                |
| `X-RateLimit-Remaining` | Requests left in the current window        |
| `X-RateLimit-Reset`     | Unix time (seconds) when the window resets |

The streaming endpoint's `text/event-stream` response and the other error envelopes (`401`, `403`, `404`, `409`, `422`, `503`, `500`) do not carry these headers; only `X-Request-ID` is on every response. If you pace a client on `X-RateLimit-Remaining`, treat a missing header as "unknown", not as zero.

When the window is exhausted the API answers `429 rate_limit_exceeded` and adds `Retry-After` (seconds until the window resets).

```http theme={null}
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1789470060
Retry-After: 17
X-Request-ID: 7f20a5f0-5dde-48cf-94aa-38acbbd218e0

{"error": {"code": "rate_limit_exceeded", "message": "Rate limit exceeded for this API key; try again later", "request_id": "7f20a5f0-5dde-48cf-94aa-38acbbd218e0"}}
```

## When the limiter itself is down

The limiter fails **closed**. If its backing store is unavailable, the API answers `503 api_rate_limit_unavailable` with `Retry-After: 5` instead of letting requests through unmetered, and instead of a misleading 429. Treat it like any other transient error: wait and retry with the same `Idempotency-Key`.

<Warning>
  Do not spread traffic across several keys of the same agent to lift the ceiling. Each key has its own conversations, so the same end user would end up with a fragmented history. If you need a higher limit, contact us.
</Warning>

## Client-side pacing

<CodeGroup>
  ```bash curl theme={null}
  # Retries on 429/503 after Retry-After, then prints the window state of the final answer.
  while :; do
    status=$(curl -s -o /dev/null -D headers.txt -w '%{http_code}' \
      "$CHATTLER_BASE_URL/v1/agents/$CHATTLER_AGENT_ID/conversations?limit=20" \
      -H "Authorization: Bearer $CHATTLER_API_KEY")
    if [ "$status" = "429" ] || [ "$status" = "503" ]; then
      sleep "$(grep -i '^Retry-After:' headers.txt | tr -d '\r' | awk '{print $2}')"
      continue
    fi
    grep -i '^X-RateLimit-' headers.txt
    break
  done
  ```

  ```python Python theme={null}
  import time

  import httpx


  def request_with_pacing(method: str, url: str, **kwargs) -> httpx.Response:
      while True:
          response = httpx.request(method, url, headers=HEADERS, timeout=120, **kwargs)
          if response.status_code in (429, 503):
              time.sleep(float(response.headers.get("Retry-After", "5")))
              continue
          if int(response.headers.get("X-RateLimit-Remaining", "1")) == 0:
              reset_at = int(response.headers.get("X-RateLimit-Reset", "0"))
              time.sleep(max(0.0, reset_at - time.time()))
          return response
  ```

  ```javascript Node theme={null}
  async function requestWithPacing(url, init = {}) {
    for (;;) {
      const response = await fetch(url, {
        ...init,
        headers: { Authorization: `Bearer ${apiKey}`, ...(init.headers ?? {}) },
      });
      if (response.status === 429 || response.status === 503) {
        const wait = Number(response.headers.get("Retry-After") ?? 5);
        await new Promise((r) => setTimeout(r, wait * 1000));
        continue;
      }
      if (response.headers.get("X-RateLimit-Remaining") === "0") {
        const resetAt = Number(response.headers.get("X-RateLimit-Reset") ?? 0) * 1000;
        await new Promise((r) => setTimeout(r, Math.max(0, resetAt - Date.now())));
      }
      return response;
    }
  }
  ```
</CodeGroup>
