ARCNM

Building blocks

Rate limits

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

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.


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:

    {
      "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 so a retry can't double-charge.

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")
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`)
}
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

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 [email protected] with your use case and we'll size it to your forecast.


See also