TTS API rate limits: 60 vs 120 RPM and Retry-After — Audra
Batch workloads hit rate limits only two ways: startup thundering-herd (fifty parallel renders at once) and polling that hammers a status endpoint. Audra's limits are generous for batch — 60 requests/minute free, 120 commercial — but the shape of your requests matters more than the number.
This post is the numbers, the Retry-After contract, and the request-shaping strategy for long-form pipelines.
The actual limits
- Speech (free): 60 requests/minute per API key.
- Speech (commercial/paid): 120 requests/minute.
- Signup: 5 accounts/hour per IP. Public demo: 10 requests/hour per IP.
- Key creation 10/hour, rotation 5/hour per account — you will not hit these in a render loop.
One nuance: a request is a request, not a character count. A 50k-character sync render is one request against the minute budget — which is why fewer, larger requests win, the opposite of realtime API instincts.
When you hit 429
The response carries a Retry-After header in seconds. Respect it literally: sleep that long on the key that was limited, then retry the same request. Do not compute your own backoff — the server tells you when it will accept traffic again, and guessing just extends the throttling. Repeatedly ignoring Retry-After is the only way to turn a 429 into a longer block.
Request shaping for long-form
curl
# Long-form pipeline: few large requests, sequential
for section in sections:
curl https://audratalks.com/v2/speech \
-H "Authorization: Bearer sk-audra-..." \
-H "Content-Type: application/json" \
-d "{\"model\":\"audra-essential\",\"text\":\"$(cat $section)\",\"voice\":\"june\"}" \
--output "$section.mp3"
sleep 1.1
Python
import requests, time
# One request per chapter, up to 50k chars each - sequential, 1/sec
for chapter in chapters:
resp = requests.post(
"https://audratalks.com/v2/speech",
headers={"Authorization": "Bearer sk-audra-..."},
json={"model": "audra-essential", "voice": "june", "text": chapter, "format": "mp3"},
timeout=3600,
)
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", "30"))
time.sleep(wait)
continue # re-enqueue this chapter
resp.raise_for_status()
JavaScript
// Few large requests beat many small ones; space them out.
for (const chapter of chapters) {
const res = await fetch("https://audratalks.com/v2/speech", {
method: "POST",
headers: { Authorization: "Bearer sk-audra-...", "Content-Type": "application/json" },
body: JSON.stringify({ model: "audra-essential", voice: "june", text: chapter, format: "mp3" }),
});
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") ?? 30) * 1000;
await new Promise((r) => setTimeout(r, wait));
continue; // re-enqueue this chapter
}
if (!res.ok) throw new Error(await res.text());
await writeFile(out, Buffer.from(await res.arrayBuffer()));
}
Why fewer, larger requests win
Splitting a 45k-character script into 100 requests of 450 chars to 'parallelize' produces exactly nothing faster — each render still takes its synthesis time — while burning 100 of your 60-per-minute budget and creating 99 chunk joins that can glitch. Send the 45k as one request; the render time is a function of characters, not request count. Parallelism in batch narration has no payoff.
The auto-queue interaction
Sync requests above roughly 8,000 characters are auto-converted to background jobs unless you send X-Audra-Sync: true. So in practice, large batch requests leave the 429 pressure zone entirely — job creation is a fast enqueue, and the long renders happen off the synchronous path. The async jobs post covers that lifecycle.
Failure mode: the polling loop
The most common self-inflicted 429 in practice is not render volume — it is a poll-every-500ms status loop on /v2/speech/jobs/{id}. Poll every 30–60 seconds, or better, use webhook_url and stop polling altogether.
Error code reference including 413 on oversized sync text: /docs#errors.