Skip to main content
The limits of a WebSocket session come first; those of the HTTP API follow. Each WebSocket limit below ends the session with an Error frame whose error_code equals the WebSocket close code that follows, with two exceptions: text intake pacing slows you down without closing anything, and the WebSocket message size is enforced by the WebSocket layer itself with a bare 1009 close. The 3006, 3010 and 3011 texts quote the number in force, and Begin.configuration.limits lists the values of your session; see Read the configuration back.

Splitting long text

A single Generate carries at most 2000 characters, counted as Unicode code points. One character over and the session closes with 3006, error_type invalid_message and param Generate.text:
The SDKs apply the limit before sending. generate() refuses text over it, with ValueError in Python and RangeError in JavaScript, and speak() splits a longer text into pieces of at most 2000 code points at code-point positions, not at word or sentence ends, so split long text at sentence ends yourself before you call speak(). Longer text goes across several Generate frames. The server joins them exactly as sent, with no separator, so include the space or newline at each edge yourself. Generate boundaries do not affect synthesis, so on their own they can fall anywhere, even in the middle of a sentence. Where you Flush is a separate decision: you can send several Generate frames before one Flush, and that Flush concludes everything buffered so far. Place flushes at sentence or clause boundaries so that a request never ends mid-sentence. If you flush after every chunk, as the program below does, that means splitting into sentences. The program below speaks a passage one sentence per request and keeps a bounded number of flushes in flight, topping up each time a FlushDone arrives. It is complete and runs as written.
The output looks like this. The durations vary between runs, because synthesis is not deterministic:
Three things the program relies on:
  • Flushes are answered in order, so you can keep a few in flight. Every Flush is queued and concluded by its own FlushDone, in flush_id order. The server works through the queue one request at a time, so each request you queue adds to the wait before its own audio starts; two or three in flight keeps the pipeline full without a long queue.
  • FlushDone means delivered, not played. It arrives once all the audio for that request has been written to your socket, which is well before playback ends because synthesis runs several times faster than real time. Topping up on FlushDone paces you against delivery, and delivery is what the limits in the table measure. Pacing playback is your player’s job; see Audio formats on WebSocket API basics.
  • Terminate flushes for you. Flushes still in flight drain before Termination arrives, and text you sent but never flushed is synthesized and gets its own FlushDone first as well. That is why the program terminates without waiting for its last FlushDone frames itself.
Flushing per sentence matters when text arrives over time, for example from an LLM: flush each sentence as it completes rather than waiting for the whole reply. When the whole passage is already in hand, a single Flush starts audio just as quickly: audio streams as it is synthesized either way, and once enough complete sentences are buffered the server begins speaking before the Flush even arrives (see Generate). What per-sentence flushing buys you in that case is a FlushDone boundary per sentence to pace on.

Pacing

The server accepts text several times faster than it can be spoken: about 100 characters per second sustained, after a 1500-character allowance at the start of the session. Past that it forwards text to synthesis at that rate rather than closing, and you see it as FlushDone frames arriving more slowly. Only a client that runs far ahead of what has been synthesized is closed: once more than 8000 characters have been sent but not yet synthesized, the session ends with 3010.
Flush, KeepAlive, Cancel and a Generate with empty text are control frames with a budget of their own: 20 per second sustained, with a burst allowance of about 60. A Generate with text and Terminate are not counted. Exceeding it also closes 3010, with a different text:
This is the limit a tight loop hits first. Sending one Flush per sentence for a whole document in a single burst spends the burst allowance in milliseconds: a 70-sentence document of under 2000 characters is closed before any audio arrives. The bounded in-flight loop above never gets near either limit, because it can only send as fast as FlushDone frames come back. A voice agent rarely meets these limits either, since its text is produced at conversational speed; flush each sentence as it completes and do not send KeepAlive while a session is busy. The same applies in the other direction. If you stop reading from the socket while audio is still being sent, the server ends the session with 3011 once 300 seconds of synthesized audio are waiting for delivery. A client that has stopped reading entirely usually never receives that frame and sees an abnormal 1006 close instead. Read from the socket continuously and buffer audio locally; your player, not the socket, should be what waits.
The values above are the current defaults and may change. The 3010 and 3011 Error texts state the limit in force, so log them. Build your retry and chunking logic on the close code you receive rather than on a hardcoded rate.

Session lifetime

A session ends at Begin.expires_at regardless of activity. That is 3600 seconds after connect by default, and earlier when a temporary token caps the session with max_session_duration_seconds; a token can shorten the hour but never extend it. expires_at is a Unix timestamp in seconds, so treat it as the authoritative deadline. When it passes, the server stops accepting input, flushes any text you have not flushed, delivers the audio still in flight along with its WordBoundaries, and then closes with 3008, at most about a minute later. There is no Termination frame on this path; the Error frame is the last message:
If you hold a socket open for a long-lived UI, reconnect on 3008 rather than assuming the connection lasts as long as the page. It is routine, not a failure. Sessions are also counted per account, the way Streaming Speech-to-Text counts them: by new sessions per minute, never by sessions open at once. A voice agent call opens one session and keeps it for the whole call, so your limit is the number of calls that can start in a minute, and calls in progress do not count against it. With a limit of 100, for example, 100 calls can start in one minute and 110 in the next, and all 210 stay connected. Calls in progress follow from your call starts and your call length: at a steady 100 call starts a minute with 4-minute calls, about 400 calls are in progress. They are still subject to the service’s capacity (3005 with error_type at_capacity) and to the one-hour session lifetime (3008), after which the reconnect is a new session. Your starting limit is your Streaming Speech-to-Text limit (5 on a free plan, 100 or more on a paid plan), counted separately: you can start that many speech-to-text sessions and that many TTS sessions in the same minute. Your limit is shown on the dashboard, and on a paid plan it scales with use as Rate limits describes; a raised starting limit can take one busy minute to apply. The limit counts new sessions in fixed one-minute windows, each running from second :01 of a minute to second :00 of the next. It is checked at connect, before Begin, and a session over it closes 3009 with a text such as Too many sessions opened for this account in the last minute (allowance 100 per minute); retry in 17 s. The number in that text is your starting limit; on a paid plan the limit in force can be higher. The Error frame’s retry_after_seconds, between 1 and 60, is the time until the next window starts, when opening a session works again. Every reconnect is a new session, including those after 1012 and 3008, and each region counts its own. Our deploys wait for sessions in progress to end; when a server has to stop sooner, it closes its own sessions (1012, or 1006 when its machine is replaced), a share of your sessions rather than all of them, so keep some room under your limit for those reconnects. Reuse one socket for many requests instead of opening a session per utterance, and back off before reconnecting. A session also closes when two minutes pass with no frame in either direction, which releases capacity when a user goes quiet. Any frame starts the two minutes again, yours or the server’s, and KeepAlive exists to do that while you have nothing to say: send one at least every two minutes to hold an idle socket open. Setting inactivity_timeout closes idle sessions sooner, after as little as 5 seconds, which is worth doing when you open a socket per user and want the capacity back quickly. When the timeout passes the session closes with 3006 and a text that begins No message received for. That is not an input error, so reconnect when you next have something to say. Begin.configuration.inactivity_timeout echoes the timeout in effect, 120 when you set none. Separately from all of this, the server sends a WebSocket ping every 60 seconds and closes with 1011 if it is not answered within 60 seconds. Browsers and mainstream libraries answer pings automatically, so only a hand-rolled client needs to care, and KeepAlive has nothing to do with it.

HTTP API limits

The HTTP API handles one text per request, so it has no pacing, idle or text backlog limits. A limit reached before the first audio answers an HTTP error; one reached after it cuts the response, or ends /v1/speech/events with an Error event: See HTTP API basics for how requests are counted.