The public API applies two layers of throttling: a token bucket on every authenticated request, and a tighter per-route cap on the small number of routes that start expensive background work.
Per-token bucket
Every authenticated request draws from one bucket per token.
| Property | Value |
|---|---|
| Capacity | 120 requests |
| Refill | 1 request per second |
A client that stays at or below 1 request per second never depletes the bucket. A burst may run up to 120 requests before the bucket empties, after which the sustained rate applies.
Requests that fail authentication draw from a separate bucket keyed on client address, with a capacity of 60 and the same refill rate. That bucket exists to blunt token guessing and is consumed before any token lookup runs.
Per-route caps
These apply in addition to the bucket, and are keyed on the tenant and token.
| Route | Cap |
|---|---|
POST /api/v2/reports | 6 per minute |
POST /api/v2/scans | 6 per minute |
POST /api/v2/rag-runs | 6 per hour, per asset |
POST /api/v2/cloud-posture-runs | 2 per day |
Each of these starts a worker job. The cap reflects how expensive the job is, not how expensive the HTTP request is.
POST /api/v2/reports carries a second control that is not a rate limit: at most 5 reports per tenant may be queued or generating at once. A caller inside the per-minute cap can still queue work faster than the worker drains it, so requests beyond that depth are refused with report_queue_full. Wait for a report to reach ready or failed before queueing another.
Request size and shape
Request bodies on write routes are capped at 1 MiB. A larger body is rejected with 413 and {"error": "payload_too_large", "limit_bytes": 1048576} before the handler runs.
Write routes also require the body to be a JSON object when one is sent:
| Condition | Response |
|---|---|
| Body absent or empty | Accepted; route defaults apply |
Content-Type is not JSON | 415 unsupported_media_type |
| Body does not parse as JSON | 400 invalid_json |
| Body parses but is a list or scalar | 400 invalid_json_object |
Nesting deeper than 512 levels does not parse and is answered with 400 invalid_json. These checks run before the handler, so a malformed request never reaches business logic and never consumes a per-route slot.
Response headers
Authenticated responses carry the bucket state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Bucket capacity |
X-RateLimit-Remaining | Requests left in the bucket |
X-RateLimit-Reset | Seconds until the bucket is full again |
Handling 429
HTTP/1.1 429 Too Many Requests
Retry-After: 54
Content-Type: application/json
{
"error": "rate_limited",
"retry_after_seconds": 54
}
Retry-After and retry_after_seconds carry the same value. Wait for it rather than retrying on a fixed interval. A rejected request does not consume further budget, so backing off cleanly is enough to recover; there is no additional penalty for a client that retries too early, and no circuit breaker that disables a route platform-wide.
Queue-depth rejections also return 429, with error set to report_queue_full and no Retry-After. Poll an in-flight report instead of retrying immediately.
Polling asynchronous work
POST /api/v2/scans and POST /api/v2/reports return as soon as the job is queued. Poll the per-resource GET route to follow progress. Four seconds is a reasonable interval; anything under one second is wasted against the bucket. For high-volume integrations, use webhooks and treat polling as a fallback.
Related
Last reviewed: 2026-08-04. Reviewed by: Engineering. Content type: Developer documentation. Reach the maintainers: [email protected] .