Rate limits
Two different refusals, and the API says which. 429 rate_limited means slow down — nothing was
consumed and the same request will succeed shortly. 429 quota_exceeded means you are out — an
allowance is spent and time or an operator is what changes it. A client that treats them alike will
retry a spent quota until its own timeout.
The ceilings
A token bucket per key and per account, refilling continuously. A request costs one token.
| Scope | Burst | Sustained |
|---|---|---|
| Per key, all endpoints | 120 | 60 / minute |
| Per key, POST /v1/runs | 20 | 10 / minute — submission spends GPU seconds |
| Per key, POST /v1/destinations/{id}/test | — | 6 / minute |
| Per account, all endpoints | 300 | 180 / minute — five keys cannot multiply the footprint by five |
| Per account, POST /v1/uploads/{id}/parts | 600 | 600 / minute — a recorder draining a backlog is a legitimate burst |
| Unauthenticated, per IP (GET /v1/health) | 60 | 30 / minute |
Both the key and the account ceiling are checked and the tighter one answers; detail.scope says
which, so a program that hit the account ceiling does not spend an afternoon looking at its own key.
The headers, on every response
curl -sS -i https://<host>/v1/runs?limit=1 -H "Authorization: Bearer $SCRIPTRIP_KEY"
HTTP/1.1 200 OK
X-Request-Id: req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55
RateLimit-Limit: 60
RateLimit-Remaining: 41
RateLimit-Reset: 37
Cache-Control: no-store
Content-Type: application/json; charset=utf-8
RateLimit-Limit is the sustained rate per window, RateLimit-Remaining the tokens left — which
can exceed the limit, because every bucket holds a burst of up to twice its sustained rate —
RateLimit-Reset the seconds until the bucket is full. When two ceilings apply the headers describe
the one with fewer tokens remaining — the one that will refuse first. Pace on these and you will
never see a 429.
When you are refused
curl -sS -i -X POST https://<host>/v1/runs -H "Authorization: Bearer $SCRIPTRIP_KEY" \
-H "Content-Type: application/json" -d '{"source":{"type":"youtube","url":"https://youtu.be/dQw4w9WgXcQ"}}'
HTTP/1.1 429 Too Many Requests
Retry-After: 6
RateLimit-Limit: 10
RateLimit-Remaining: 0
RateLimit-Reset: 6
{"error":{"code":"rate_limited","message":"This key has submitted more than 10 runs in the last minute.","detail":{"scope":"key","limit":10,"window_seconds":60},"request_id":"req_0195c8e4-9a10-7d3e-8f21-0b6c4d2e7a55"}}
Retry-After is always delta seconds, never a date, and never more than the window. Honour it.
A recommended backoff
- On
429 rate_limitedand503 engine_unavailable: waitRetry-Afterseconds, then retry the same request. Back off exponentially from there if it repeats, capped at a minute. - On
500 internal_error: retry with exponential backoff starting at one second, at most a few times, then quoterequest_idto us. - On
429 quota_exceeded: do not loop.Retry-Afteris honest and may be days. - On any other
4xx: never retry; the request is wrong and will be wrong again.
The generated TypeScript client does exactly this.
Quotas
| Ceiling | Refused with |
|---|---|
| Monthly transcribed seconds | 429 quota_exceeded at POST /v1/runs, detail: {limit_seconds, used_seconds} |
| Concurrent open uploads (5) | 429 quota_exceeded at POST /v1/uploads |
| Bytes per day (20 GiB) | 429 quota_exceeded at POST /v1/uploads |
| Bytes per media object (2 GiB) | 413 payload_too_large |
| Seconds per run (8 hours) | 422 unprocessable_source, or a run failed too_long |
| Destinations per account (20) | 429 quota_exceeded at POST /v1/destinations |
The monthly check is against seconds already recorded, not the run being submitted; a run that crosses the ceiling mid-flight finishes, and the next submission is refused. Cached seconds do not count.