# Rate limits

> Each API key gets 300 requests a minute; when you go over, wait for Retry-After and try again.

## Know the limits

| Limit | Scope |
| --- | --- |
| 300 requests a minute | Each API key, across REST and MCP |
| 300 requests a minute | All delegated agent credentials of one app and environment, together |
| 600 requests a minute | Each network address, checked before authentication, per server |
| 120 requests a minute | Each network address on the demo API, by default |

`GET /health` isn't limited.

## Read the headers

Authenticated responses carry your remaining budget. Some error responses may not.

| Header | Meaning |
| --- | --- |
| `RateLimit-Limit` | The limit for this key. |
| `RateLimit-Remaining` | Requests left in this window. |
| `RateLimit-Reset` | When the window resets, in Unix seconds. On the demo API, it's seconds from now. |

## Handle a 429

The REST API returns `429` with code `rate_limited` and a `Retry-After` header in seconds. Wait at least that long, then retry.

Node.js:

```js
// For GET requests. Retries 429, 5xx, and network errors, and honors Retry-After.
async function getWithRetry(url, headers, attempts = 5) {
  for (let attempt = 1; ; attempt += 1) {
    const res = await fetch(url, { headers, signal: AbortSignal.timeout(30_000) }).catch((error) => error);
    const failed = res instanceof Error || res.status === 429 || res.status >= 500;
    if (!failed) return res;
    if (attempt === attempts) {
      if (res instanceof Error) throw res;
      return res;
    }
    const retryAfter = res instanceof Error ? 0 : Number(res.headers.get('Retry-After')) || 0;
    const backoff = Math.min(30, 2 ** attempt) + Math.random();
    await new Promise((resolve) => setTimeout(resolve, Math.max(retryAfter, backoff) * 1000));
  }
}
```
Python:

```python
import random
import time

import requests

def get_with_retry(url, headers, attempts=5):
    """For GET requests. Retries 429, 5xx, and network errors, and honors Retry-After."""
    for attempt in range(1, attempts + 1):
        try:
            res = requests.get(url, headers=headers, timeout=30)
        except requests.RequestException:
            if attempt == attempts:
                raise
            res = None
        if res is not None:
            retryable = res.status_code == 429 or res.status_code >= 500
            if not retryable or attempt == attempts:
                return res
        retry_after = float(res.headers.get("Retry-After", 0) or 0) if res is not None else 0
        time.sleep(max(retry_after, min(30, 2 ** attempt) + random.random()))
```

Use these for reads only:

- Retry `POST /connect/sessions` only with the same `Idempotency-Key` and the same body.
- Don't retry sandbox controls blindly. Each call changes the synthetic patient.
- Don't retry other `4xx` responses blindly. Handle them by code; `403 authority_changed`, for example, asks you to retry the read once.

Over MCP, a key or address limit fails the whole request with a JSON-RPC error and HTTP `429`. A throttled tool returns `retryAfterSeconds` in its error metadata.

## Stay under the limit

- Page with `limit=100` instead of many small pages.
- Read the [change feed](/docs/records/change-feed) instead of re-reading whole categories.
- Use webhooks instead of polling sessions.
