Skip to main content
Lesson 9 of 14 · Grok Voice & Audio

Streaming TTS over WebSocket

Lesson 9 of 14 Updated November 3, 2026 Intermediate
On this page
  1. Why streaming changes the experience
  2. Open the connection
  3. The message protocol
  4. A complete implementation
  5. Limits to know
  6. HTTP or WebSocket
  7. Key takeaways

Updated November 3, 2026, based on the xAI Text to Speech page. Source: Text to Speech.

Why streaming changes the experience

In HTTP mode, TTS only hands control back once all the audio has been produced. For a two-sentence notification, nobody notices the wait. For a blog post, a book chapter, or a continuously scrolling news feed, the wait becomes your product’s biggest flaw: users click, then stare at a loading spinner for several seconds.

WebSocket mode removes that wait by pushing audio out as it’s synthesized. Playback starts on the first words while the end of the text hasn’t been processed yet. That’s the difference between a page that seems frozen and audio that plays right away.

Open the connection

The connection opens on wss://api.x.ai/v1/tts, and all the configuration goes in the URL’s query string rather than in an initialization message.

const ws = new WebSocket(
  "wss://api.x.ai/v1/tts?language=en&voice=ara&codec=mp3&sample_rate=24000",
  {
    headers: {
      "Authorization": "Bearer " + XAI_API_KEY
    }
  }
);

These are the accepted parameters:

  • language (required): a BCP-47 code or auto
  • voice (optional, default: eve): the voice ID
  • codec (optional, default: mp3): the output audio format
  • sample_rate (optional, default: 24000): the sample rate in Hz
  • bit_rate (optional, default: 128000): the bit rate in bps (MP3 only)

These settings apply for the whole session: you can’t change the voice or codec along the way. If your app needs to alternate between two narrators, open two connections.

The message protocol

On the sending side, two messages are all you need to drive synthesis. The first sends a chunk of text; the second announces that the text is complete.

{ "type": "text.delta", "delta": "Hello and welcome " }
{ "type": "text.done" }

You can send as many text.delta messages as you need, each up to 60,000 characters. Still, it’s better not to max out that limit: splitting the text into chunks of a few sentences gets the first audio chunk to you much sooner, which is the whole point of streaming. A single delta of several thousand characters takes you back to HTTP-mode behavior.

On the receiving side, each audio.delta carries a fragment of base64-encoded audio, in the codec you set when connecting. Decode it and append it to your playback buffer.

{ "type": "audio.delta", "delta": "<base64 audio>" }
{ "type": "audio.done", "trace_id": "trace-uuid" }

The audio.done event marks the end of synthesis. Its trace_id is the identifier to give support, or to record in your logs, when a generation misbehaves. If the text is invalid or you’ve exceeded your quota, you get an error message instead.

{ "type": "error", "message": "description of the error" }

A complete implementation

The example below splits a long text into paragraphs, sends them all, then assembles the audio fragments it receives into an MP3 file.

const WebSocket = require("ws");
const fs = require("fs");

const ws = new WebSocket(
  "wss://api.x.ai/v1/tts?language=en&voice=rex&codec=mp3",
  { headers: { "Authorization": "Bearer " + process.env.XAI_API_KEY } }
);

const audioChunks = [];

ws.on("open", () => {
  const paragraphs = longText.split("\n\n");
  for (const para of paragraphs) {
    ws.send(JSON.stringify({
      type: "text.delta",
      delta: para + " "
    }));
  }
  ws.send(JSON.stringify({ type: "text.done" }));
});

ws.on("message", (data) => {
  const msg = JSON.parse(data);
  if (msg.type === "audio.delta") {
    audioChunks.push(Buffer.from(msg.delta, "base64"));
  } else if (msg.type === "audio.done") {
    fs.writeFileSync("output.mp3", Buffer.concat(audioChunks));
    ws.close();
  }
});

Note the para + " ": without that space, the end of one paragraph runs into the start of the next, and the engine reads the two words together without a breath.

Limits to know

MetricValue
Text per delta60,000 characters max
Total textUnlimited
Concurrent sessions50 per team
Session permit600 seconds
Connection timeoutNone

The xAI page says the connection has no timeout, and separately mentions a “session permit” of 600 seconds without explaining what happens when it runs out. To be safe, connect when you have text to send rather than when the page loads, and plan for reconnection. Plan around the limit of 50 concurrent sessions per team the same way: an audio library converting its catalog in the background should queue its jobs rather than open two hundred connections in parallel.

HTTP or WebSocket

HTTP mode is still the right choice for short texts, one-off generations, and anywhere integration simplicity matters most: one request, one response, no state to manage. WebSocket is the way to go once the text gets long, when the audio must play while it’s being produced, or when the content itself arrives as a stream, typically the output of a language model that you voice as it generates.

Key takeaways

  • WebSocket TTS lets you receive streamed audio without waiting for generation to finish
  • Configuration parameters go in the query string of the connection URL
  • The protocol is simple: text.delta to send text, audio.delta to receive audio
  • WebSocket sessions are limited to 50 concurrent per team; plan for reconnection
  • Prefer WebSocket for long texts and real-time listening experiences