Rate limits
Rate limits protect the service from bursts. They are separate from plan limits such as the number of jobs open at once, which return 403 PLAN_LIMIT_EXCEEDED. See Errors.
How limits work
Section titled “How limits work”Each limit is a token bucket. The bucket holds up to burst requests and refills at a steady rate. You can spend a whole burst at once, then you are held to the rate. After a pause the bucket fills up again.
Every request passes through several limits in this order. The first one that is empty rejects the request.
api_key_auth_ip: per IP address, before the key is checked.api_key: per API key.api_user: per account, shared by all of its keys.- A limit for the route, if it has one:
media_create,media_partsorexport.
GET /plans and GET /languages need no key and use api_anon_ip per IP address instead.
| Policy | Counted per | Rate | Burst | Applies to |
|---|---|---|---|---|
api_key_auth_ip |
IP address | 20 per second | 60 | Every request that carries a well-formed key |
api_key |
API key | 10 per second | 40 | Every keyed request |
api_user |
Account | 20 per second | 100 | Every keyed request |
api_anon_ip |
IP address | 5 per second | 30 | GET /plans, GET /languages |
media_create |
Account | 60 per minute | 50 | POST /media |
media_parts |
Account | 10 per second | 40 | POST and GET /media/{id}/parts |
export |
Account | 30 per minute | 10 | GET /transcriptions/{id}/export |
IPv6 addresses are counted per /64 network, not per single address.
Because api_user is per account, several keys do not multiply your limit. Splitting work across keys helps you tell services apart, not go faster.
If the rate limiter itself is not available, the api_key_auth_ip check fails closed: the request gets 503 SERVICE_UNAVAILABLE instead of being let through. Retry with backoff.
Headers
Section titled “Headers”Responses that pass through a limit carry two headers about the last limit they were checked against.
| Header | Meaning |
|---|---|
RateLimit-Limit |
The size of that limit’s burst. It is not the refill rate. |
RateLimit-Remaining |
How many requests are left in the bucket right now. |
When a request is rejected, the response also has:
| Header | Meaning |
|---|---|
Retry-After |
Whole seconds to wait before you try again. It is at least 1. |
The 429 response
Section titled “The 429 response”HTTP/1.1 429 Too Many RequestsRetry-After: 2RateLimit-Limit: 40RateLimit-Remaining: 0Content-Type: application/json; charset=utf-8{ "success": false, "code": 429, "message": "Too many requests. Please slow down.", "data": null, "error": { "code": "RATE_LIMITED", "details": { "policy": "api_key", "retryAfterSeconds": 2 } }, "meta": { "requestId": "019a4f30-1c22-7a10-b5d3-42f1c0a8e777", "timestamp": "2026-12-01T09:41:07.512Z" }}error.details.policy names the limit that rejected the request, and retryAfterSeconds repeats the Retry-After header.
Staying under the limits
Section titled “Staying under the limits”- Wait for
Retry-Afterseconds after a429. Then retry the same request. - Add random jitter to your waits when many workers share a key, so they do not retry at the same instant.
- Do not poll a transcription in a tight loop. Poll every few seconds, or use webhooks and skip polling.
- Upload parts in a small number of parallel streams. Request presigned URLs just before you use them, in batches of up to 100 part numbers, instead of one call per part.
- Read segments with a large
limitandincludeWords=falsewhen you only need text. See Segments. - Cache
GET /plansandGET /languages. They change rarely.
Can't find what you need? Contact support, email [email protected], or open Help and then Contact in the app. scribewren.com