Skip to content

Limits, errors & 503

Request limits

Limit Value Response when exceeded
Text length 4 000 characters (after trimming) 413 {"error":"text longer than 4000 characters"}
Chunks per request 64 (text is split into ≤ 50-token sentence chunks) 413 {"error":"text splits into N chunks; max 64"}
Sample rate 24 000 only 400
speed 1.0 only (the model has no rate control) 400
temperature 0 – 2 (default 0.3) 400
Request timeout 120 s end to end stream ends; WS sends {"type":"error","error":"timeout"}
Active keys per organisation 50 portal refuses to create more

There is no per-key rate limit. Concurrency is the real limit: one stream per worker, one worker per vCPU, N nodes. Requests that find every worker busy wait in a short queue (up to 8 requests, at most 2 s); past that they are refused rather than delayed.

Status codes

Status Meaning What to do
200 audio follows (chunked) play / save; x-request-id identifies the request in your usage log
400 bad request: unknown voice, bad format, nothing left to say after normalisation, bad temperature/speed/sample_rate fix the request; do not retry as-is
401 missing, unknown or revoked key check x-api-key; rotate in the portal
403 voice not in this key's allow-list use an allowed voice or a different key
413 text too long or too many chunks split the text (sentence boundaries work best)
503 + Retry-After: 1 every worker busy and the queue full, or the queue wait exceeded 2 s retry after Retry-After seconds with jitter; nothing was billed
500 engine error retry once; report the x-request-id if it repeats

Errors are JSON: {"error": "<message>"}. Status-code semantics are the same on the WebSocket, as an event: {"type":"error","request_id":…,"status":503,"retry_after":1,"error":"busy"} (validation errors carry their status too).

The 503 contract

We would rather tell you now than make your caller wait. When capacity is exhausted:

  1. REST returns 503 Service Unavailable with Retry-After (seconds, currently 1) before any audio, so your code can fall back or retry cleanly. A stream that has started is never cut short for capacity reasons.
  2. WebSocket sends an error event with status: 503 and retry_after for that utterance; the connection stays open and later utterances are unaffected.
  3. Nothing is logged to usage or billed.

Retry recipe: wait Retry-After + 0–250 ms jitter, retry up to three times, then degrade (cached audio, a shorter line, a text fallback). For planned load, tell us: capacity is added by adding nodes behind the load balancer, not by queueing on one.

import random, time, requests
def tts(body, tries=3):
    for i in range(tries):
        r = requests.post(URL, headers=H, json=body, stream=True)
        if r.status_code != 503: r.raise_for_status(); return r
        time.sleep(float(r.headers.get("retry-after", "1")) + random.random() / 4)
    raise RuntimeError("TTS busy")

Timeouts and cancellation

  • Closing the HTTP connection (or the WebSocket) cancels synthesis within one frame; you are billed only for audio already produced.
  • Per-request wall-clock limit: 120 s. A 4 000-character request is ~4 minutes of audio and finishes in well under a minute.
  • Keep WebSocket connections open across turns; opening one costs a TLS handshake, not synthesis time.

Health

GET /readyz is 200 ready only when every worker has warmed up (else 503 workers warming up); GET /healthz reports uptime and worker counts; GET /metrics is Prometheus text (vtts_streams_active, vtts_queue_depth, vtts_ttfa_seconds, vtts_rtf, vtts_audio_seconds_total, vtts_requests_total{endpoint,status}, vtts_errors_total{kind}). Live view: portal status page.