Rate Limits & Quotas
Two independent budgets govern every call: a per-route token bucket (QPS) and a plan-level credit quota (monthly spend). Both surface as HTTP 429 with explicit Retry-After guidance.
Plan budgets
| Plan | Req / minute | Concurrent jobs | Monthly credits |
|---|---|---|---|
| Free | 60 | 1 | 200 |
| Developer | 600 | 4 | 5 000 |
| Growth | 1 800 | 16 | 50 000 |
| Enterprise | custom | custom | pooled |
Per-route ceilings
Even on unlimited-plan tenants, each route carries a safety ceiling to prevent queue-bomb or polling-loop abuse. Keep long-running jobs asynchronous — don't poll in a tight loop.
| Route | Budget | Notes |
|---|---|---|
| POST /v1/client/jobs | 1 request / sec / tenant | Job submission — controlled to prevent queue-bomb DoS. |
| POST /v1/client/datasets/upload | 10 req / min / tenant | Covers multipart + profiling; larger datasets bill per MB. |
| POST /v1/client/seals | 60 req / min / tenant | Mint new sealed contract seal. |
| GET /v1/client/jobs/:id | 60 req / min / key | Polling — use webhooks where possible to avoid burning this budget. |
| POST /v1/client/agent/projects | 10 req / min / tenant | ADS project creation. Plan + approve + stream reuse the same budget. |
| GET /v1/client/evidence/:id | 30 req / min / key | Bundle download. Cache locally; bundles are immutable. |
| POST /v1/client/webhooks | 10 req / min / tenant | Webhook registration / rotation. |
Response headers
Every response includes rate-limit telemetry so a well-behaved client can pace itself without ever hitting 429.
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1700003600
X-RateLimit-Route: GET /v1/client/jobs/:id
Content-Type: application/json- Limit — max allowed per window
- Remaining — tokens left in the current window
- Reset — Unix timestamp when the bucket refills
- Route — which budget was charged
Handling 429
When a budget trips, the response carries a Retry-Afterheader (delta-seconds or HTTP-date) and an error_code naming the budget — for an API key's per-minute limit KEY_RATE_LIMIT_EXCEEDED, whose Retry-After is the seconds left in the key's minute. A session replay read past its bound answers 429 REPLAY_TOO_MANY_AT_ONCE (too many of your reads running at once) or 503 REPLAY_BUSY, with Retry-After too. The SDK waits out a 429 or 503 that carries Retry-After as the server asks and carries on by itself — such waits do not spend max_retries and are bounded per call (60 waits, 15 minutes in all; past that the error is raised); pass on_wait= to be told of each. An upload waits for less: a busy service (503 UPLOAD_SCRATCH_BUSY) or your own uploads in progress (429) are waited out for 2 minutes in all, sending the file again each time, then raised with the server's words — set patience= on upload_dataset, or RADMAH_UPLOAD_PATIENCE (seconds; 0 does not wait); rady datasets upload --patience does the same. A preview, download or profile that needs temporary space the service is busy with answers 503 SCRATCH_SPACE_BUSY with Retry-After. rady prints one line on stderr while it waits. A hand-written client must honour the header.
# The SDK waits out a 429 / 503 that carries Retry-After as the server
# asks (bounded per call); without one it backs off exponentially from 1 s.
from radmah_sdk import RadMahClient
client = RadMahClient(
api_key="sl_live_…",
max_retries=5,
on_wait=lambda seconds, err: print(f"waiting {seconds:.0f} s: {err.message}"),
)
for jid in jobs:
try:
status = client.jobs.get(jid) # polls safely under backpressure
except RadMah AIError as exc:
if exc.status_code == 429:
# Every automatic retry exhausted. Quota is persistently
# exhausted — move to a webhook subscription instead of polling.
schedule_switch_to_webhooks()Patterns that burn budget
- Tight polling on
GET /jobs/:id. Prefer webhooks; if you must poll, back off exponentially once the job enters a non-terminal state. - Parallel submit loops that ignore
concurrent_jobs. Useclient.batch_create_jobs(), which respects the ceiling server-side. - Synchronous ADS streaming across dozens of keys. Open one streaming connection per project; don't re-establish on every turn.