Rate limits
Per-key, sliding window, conservative defaults. Every key gets a request budget; you can raise (or lower) it per key from the portal — no email required.
The number
Default: 60 requests per minute, per key. Every bearer-authenticated endpoint shares the same bucket.
Three knobs feed the effective limit, evaluated in order — the first one set wins:
| Where it lives | Who sets it | Overrides… |
|---|---|---|
Per-key — rate_limit_per_minute | You, from Dashboard → Keys (expand a row) or via client.keys.update(key_id, rate_limit_per_minute=…) | Org override + default |
| Per-org — set by Archie | Email [email protected] if you need an org-wide bump that applies to every new key | Default |
| Default — 60/min | (applies when neither override is set) | — |
Setting rate_limit_per_minute = 0 on a key blocks every request through it (useful for parking a key without revoking it). Setting it to null clears the override and falls back to the org / default.
Other surfaces
Beyond the per-key cap, a few endpoints have their own limits:
| Surface | Default limit | Window |
|---|---|---|
| IP-level fairness gate | 120 req | 1 min |
WorkStream cycle starts (workstream.run) | 20 starts | 1 min |
SSE event tail (/api/v1/workstreams/cycles/{id}/events) | 4 concurrent | (per key) |
| Portal key creation | 1 req per minute, 5 per day | per signed-in user |
Setting a per-key override
Two paths — same backend, same instant effect:
Portal
- Open Dashboard → Keys.
- Click the chevron on the key row to expand its detail panel.
- Set Rate limit (per minute). The save happens 600ms after you stop typing; a small
savedpill confirms.
SDK (heyarchie 1.2.0+)
from heyarchie import Archie
client = Archie()
client.keys.update("api_key_01KRVGB…", rate_limit_per_minute=30)
# Next request through that key sees the new cap immediately.Cache invalidation is automatic — the backend flushes the per-key cache on PATCH so the new cap is honoured on the very next request. No 5-minute staleness window.
Headers on every response
Every API response includes the current quota state for the caller's key:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Request budget for the window. Reflects the per-key override if set, else the org / default. |
X-RateLimit-Remaining | How many you have left in the current window. |
X-RateLimit-Reset | Unix epoch seconds when the window rolls over. |
When you're throttled
Limit-exceeded responses return HTTP 403 with the canonical error envelope and a Retry-After header (seconds):
HTTP/1.1 403 Forbidden
Content-Type: application/json
Retry-After: 23
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1747356900
{
"error": {
"type": "api_error",
"code": "RATE_LIMIT.USER",
"message": "Rate limit exceeded for user 'c79225d0-…': 30 requests per 60s",
"retry_after_seconds": 23
}
}Note: the code is RATE_LIMIT.USER when a per-key or default cap fires, and RATE_LIMIT.ORG when an org-wide override hits first.
Retry pattern
The official SDKs and CLI handle this automatically. Rolling your own client? Exponential backoff with Retry-After as the floor:
import time, random, httpx
def call_with_retry(url, headers, payload, max_attempts=5):
delay = 1.0
for attempt in range(max_attempts):
r = httpx.post(url, headers=headers, json=payload)
if r.status_code != 403 or "RATE_LIMIT" not in r.text:
return r
sleep_for = max(int(r.headers.get("Retry-After", "0")), delay) + random.random()
time.sleep(sleep_for)
delay = min(delay * 2, 30.0)
raise RuntimeError(f"Still rate-limited after {max_attempts} attempts")Pair the retry with the same idempotency_key from your original request on any write skill (workstream.run, workstream.cancel, etc.) so the server returns the original cycle id rather than starting a duplicate.
What never throttles
- Skill catalogue listing (
GET /api/v1/skills) — cached, free. - The OpenAPI spec (
/openapi.json) — static. - Health check (
/health). - OAuth discovery + dynamic client registration — bursty by nature; throttled at the network edge instead.
Next
- Authentication — sk_* bearer, scopes, where keys live.
- WorkStreams — when retries land on a cycle, use
idempotency_key. - Python SDK — handles 403 RATE_LIMIT + Retry-After for you.