Rate Limits
Amdahl enforces rate limits at several layers so that bursty traffic from one caller cannot starve the platform. This page documents what is live today, what is on the roadmap, and the client-side patterns you should adopt now so your integration behaves correctly once per-key throttles ship.
Current state
Global per-IP limiter
Every request to the platform passes through a single global limiter applied at the HTTP edge. The defaults are:
- 100 requests per minute per IP for production traffic on
/api/platform/v1/* - 500 requests per minute per IP in local development
The limiter is shared across all endpoints under the platform API. A client making 60 POST /search calls and 40 GET /chats calls in the same minute counts against the same 100-request budget.
OAuth dynamic client registration
Dynamic client registration under RFC 7591 has its own tighter bucket:
- 5 registrations per minute per IP at
POST /oauth/register
This is deliberate. Client registration is a write operation that provisions long-lived credentials, so the protection is stricter than read traffic.
Chat turn budgets
Chat runs have one soft cap that functions as a turn-based rate limit:
- The depth tier's turn budget (roughly 10 / 50 / 75 turns for
quick/standard/deep). When exhausted the run pauses with acontinue_or_finishquestion rather than ending outright — you decide whether to grant more turns or wrap up.
See Chat: depth for the tiers and Agents: resume for answering the pause.
Verb quotas
The synchronous verb families (search / enrich / lookalike / evals) can carry an operator-set monthly hard cap per workspace. Exhausting one returns quota_exceeded (429) with the family, limit, and usage in details; the cap resets at the start of the next month. Unset caps cost nothing — most workspaces never see this code.
Per-key and per-tool limits (roadmap)
Per-API-key and per-tool rate limits are planned but not yet enforced. When they land:
- Limits will be scoped to the API key, not the IP, so shared infrastructure stops being a noisy neighbor.
- Read-heavy operations (
search.run,search.query, Chat reads) will get higher budgets than operations that start work (chat.start,routines.run_now). - Burst allowances will let callers spike briefly before sustained throttling kicks in.
Until that ships, treat the 100-req-per-minute global limit as your effective ceiling and build in the headers and backoff logic below so your code keeps working when the tighter per-key limits arrive.
Response headers
Every response from the platform API carries rate-limit headers that let you monitor your budget without a separate bookkeeping layer:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Total requests permitted in the current window |
X-RateLimit-Remaining | Requests still available in the current window |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
Read these on every response, not just on errors. If X-RateLimit-Remaining is close to zero, slow down before you hit a 429.
429 response shape
When you hit a limit the platform returns HTTP status 429 with the standard error envelope:
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Try again in 42 seconds.",
"details": {
"retry_after_seconds": 42,
"limit": 100,
"window_seconds": 60
}
}
}The Retry-After header is also set, in seconds, so you can rely on either the envelope detail or the header.
Backoff strategy
When you receive a 429, back off and retry. The recommended pattern is exponential backoff with full jitter, capped at 60 seconds:
async function withBackoff<T>(fn: () => Promise<T>, maxAttempts = 5): Promise<T> {
let attempt = 0
while (true) {
try {
return await fn()
} catch (err: any) {
if (err.status !== 429 || attempt >= maxAttempts - 1) throw err
const baseMs = Math.min(60_000, 1000 * 2 ** attempt)
const jitterMs = Math.random() * baseMs
await new Promise(r => setTimeout(r, jitterMs))
attempt++
}
}
}Three rules:
- Honor
Retry-Afterfirst. If the header is present, sleep at least that many seconds before retrying. - Never retry faster than 1 second. Tight retry loops make the problem worse.
- Cap at 60 seconds. Beyond that, surface the error to the caller rather than hiding a long stall.
Best practices
- Aggregate where possible.
POST /search/querycan filter, group, and aggregate in a single call (group_by+metrics); avoid making ten requests for data one aggregation returns. - Cache surface metadata. The tool catalog, scope tables, and
GET /search/fieldsvocabulary change on the order of weeks, not seconds. Cache them on your side and refresh daily, not per-request. - Do not poll faster than 1/sec. When polling a Chat run, prefer
read_url?wait_ms=30000— one long-poll request per 30 seconds instead of thirty short ones — or subscribe to the run'sstream_url(SSE) for sub-second updates. See Chat: watching a run. - Spread start bursts. Starting hundreds of Chats or firing hundreds of routine run-nows in seconds will tip you into throttling. Spread them over a minute or enqueue them on your side.
- Key your retries by idempotency. If you retry a
POST, make sure the server-side call is idempotent or carries a dedupe key, so a successful-but-timed-out first attempt plus a successful retry do not create two records.
See also
- API reference: errors for the full error envelope and every code
- Chat for the run-level budget mechanics