---
title: Rate limits
description: Per-key and per-caller limits, the 429 envelope, and how to back off safely.
---

# Rate limits

ARCNM enforces per-key and per-caller rate limits; exceed one and you
get `429`, and every `429` costs €0. The independent limits:

| Limit | Applies to | Default ceiling |
|---|---|---|
| **Per API key** | A single key's request rate | 1,200 req/min |
| **Per-caller quota bucket** | Each caller (key / user / org), all routes | 600 req/min (sliding) |
| **Auth routes** | Login / signup / password-reset, per IP | Tight per-route limits |

When a limit is hit you get `429`. **All 429s cost €0.**

> These are current defaults and may change. Plan-tier entitlements
> (calculations/month, storage) are a separate axis — see
> [Pricing](./pricing.md).

---

## The 429 envelope

There are two 429 codes depending on which limiter fired:

- The per-caller quota bucket returns `rate_limited` with the limit and
  window in `details`:

  ```json
  {
    "error": {
      "code": "rate_limited",
      "message": "Per-org quota exceeded — retry shortly.",
      "details": { "limit": 600, "window_seconds": 60 }
    },
    "request_id": "req_…"
  }
  ```

- The auth-route limiter (login / signup / password-reset) returns
  `too_many_requests`.

> **We do not currently emit `X-RateLimit-*` or `Retry-After` headers.**
> Don't depend on them — use exponential backoff on `429` (below).

---

## Retry strategy

Exponential backoff with full jitter. Always pair retries with an
[`Idempotency-Key`](./idempotency.md) so a retry can't double-charge.

<CodeTabs>

```python title="Python"
import random, time, requests

def with_retry(fn, *, max_attempts=5, cap_s=30):
    for attempt in range(max_attempts):
        resp = fn()
        if resp.status_code != 429:
            return resp
        wait = random.uniform(0, min(cap_s, (2 ** attempt)))
        time.sleep(wait)
    raise RuntimeError(f"giving up after {max_attempts} attempts")
```

```typescript title="TypeScript"
async function withRetry(fn: () => Promise<Response>, maxAttempts = 5, capMs = 30_000) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fn()
    if (res.status !== 429) return res
    const wait = Math.random() * Math.min(capMs, 2 ** attempt * 1_000)
    await new Promise((r) => setTimeout(r, wait))
  }
  throw new Error(`giving up after ${maxAttempts} attempts`)
}
```

```bash title="cURL"
for attempt in 0 1 2 3 4; do
  resp=$(curl -sw '\n%{http_code}' "$URL" -H "X-API-Key: $ARCNM_API_KEY")
  code=$(printf '%s' "$resp" | tail -n1)
  body=$(printf '%s' "$resp" | sed '$d')
  if [ "$code" = "200" ] || [ "$code" = "201" ] || [ "$code" = "202" ]; then echo "$body"; exit 0; fi
  if [ "$code" != "429" ] && [ "$code" -lt 500 ]; then echo "$body" >&2; exit 1; fi
  sleep $(awk -v n=$((2 ** attempt)) 'BEGIN{srand(); print n*rand()}')
done
```

</CodeTabs>

---

## What counts toward the limit

- Every authenticated REST call counts toward the per-key and per-caller
  limits.
- Pre-auth calls (login, signup, password reset) hit the auth-route
  limiter.
- **MCP traffic** is handled by the MCP server's own verification and is
  not subject to the REST per-request quota bucket.
- Webhook deliveries we send to **you** don't count — those are our
  egress.

---

## Need more headroom?

If a key is hitting the ceiling, email **hello@arcnm.io** with your
use case and we'll size it to your forecast.

---

## See also

- [Errors](./errors.md) — `rate_limited`, `too_many_requests`.
- [Idempotency](./idempotency.md) — safe retries.
- [Pricing](./pricing.md) — plan entitlements and usage.
