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_limitedwith the limit and window indetails:{ "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-*orRetry-Afterheaders. Don't depend on them — use exponential backoff on429(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
- Errors —
rate_limited,too_many_requests. - Idempotency — safe retries.
- Pricing — plan entitlements and usage.