> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kugelaudio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-context streaming

> Manage up to 20 independent audio streams over a single WebSocket connection.

For advanced use cases like multi-speaker conversations or pre-buffering
audio, use the multi-context WebSocket endpoint (`/ws/tts/multi`). This allows
managing up to **20 independent audio streams** over a single connection.

## Use cases

* **Multi-speaker conversations**: Generate audio for different speakers concurrently
* **Pre-buffering**: Start generating the next response while the current one plays
* **Interleaved audio**: Dynamically switch between speakers in real-time

## Example

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  import websockets
  import json
  import base64

  async def multi_speaker_demo():
      async with websockets.connect(
          "wss://api.kugelaudio.com/ws/tts/multi?api_key=YOUR_API_KEY"
      ) as ws:
          # The first non-empty text for an ID creates its context.
          await ws.send(json.dumps({
              "text": "The story begins.",
              "context_id": "narrator",
              "voice_settings": {"voice_id": 1071},
              "flush": True,
          }))

          await ws.send(json.dumps({
              "text": "Hello, I'm the main character!",
              "context_id": "character",
              "voice_settings": {"voice_id": 1072},
              "flush": True,
          }))

          # Gracefully drain and close both contexts.
          await ws.send(json.dumps({"close_context": True, "context_id": "narrator"}))
          await ws.send(json.dumps({"close_context": True, "context_id": "character"}))

          # Receive audio from both contexts
          closed = set()
          async for message in ws:
              data = json.loads(message)

              if "audio" in data:
                  context_id = data["context_id"]
                  audio_bytes = base64.b64decode(data["audio"])
                  play_audio(context_id, audio_bytes)

              if data.get("context_closed"):
                  # Per-context usage for this conversation: audio time + charge
                  print(f"[{data['context_id']}] usage: {data.get('usage')}")
                  closed.add(data["context_id"])
                  if len(closed) == 2:
                      await ws.send(json.dumps({"close_socket": True}))

              if data.get("session_closed"):
                  break

  asyncio.run(multi_speaker_demo())
  ```

  ```javascript JavaScript theme={null}
  const ws = new WebSocket('wss://api.kugelaudio.com/ws/tts/multi?api_key=YOUR_API_KEY');

  const audioQueues = new Map();
  const closedContexts = new Set();

  ws.onopen = () => {
    // The first non-empty text for an ID creates its context.
    ws.send(JSON.stringify({
      text: 'Once upon a time...',
      context_id: 'narrator',
      voice_settings: { voice_id: 1071 },
      flush: true,
    }));

    ws.send(JSON.stringify({
      text: 'That sounds like the beginning of a story!',
      context_id: 'character',
      voice_settings: { voice_id: 1072 },
      flush: true,
    }));

    ws.send(JSON.stringify({ close_context: true, context_id: 'narrator' }));
    ws.send(JSON.stringify({ close_context: true, context_id: 'character' }));
  };

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);

    if (data.audio) {
      const contextId = data.context_id;
      const binary = atob(data.audio);
      const bytes = new Uint8Array(binary.length);
      for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);

      if (!audioQueues.has(contextId)) audioQueues.set(contextId, []);
      audioQueues.get(contextId).push(bytes);
    }

    if (data.chunk_complete) {
      console.log(`Context ${data.context_id} chunk done`);
    }

    if (data.context_closed) {
      // Per-context usage for this conversation: audio time + charge
      console.log(`[${data.context_id}] usage:`, data.usage);
      closedContexts.add(data.context_id);
      if (closedContexts.size === 2) {
        ws.send(JSON.stringify({ close_socket: true }));
      }
    }

    if (data.session_closed) {
      ws.close();
    }
  };

  ```

  ```bash cURL (websocat) theme={null}
  # Multi-context streaming requires a WebSocket client.
  # Install websocat: https://github.com/vi/websocat

  # Connect and send JSON messages interactively:
  websocat "wss://api.kugelaudio.com/ws/tts/multi?api_key=$KUGELAUDIO_API_KEY"

  # Then type each message and press Enter:
  {"text": "The story begins.", "context_id": "narrator", "voice_settings": {"voice_id": 1071}, "flush": true}
  {"text": "Hello, I'm the main character!", "context_id": "character", "voice_settings": {"voice_id": 1072}, "flush": true}
  {"close_context": true, "context_id": "narrator"}
  {"close_context": true, "context_id": "character"}
  {"close_socket": true}
  ```
</CodeGroup>

## Protocol summary

Each context message carries a `context_id`; the first message for a new ID
creates the context and can include `voice_settings`. An empty-text message can
therefore create or keep alive a context, but `voice_id` must be set no later
than the first non-empty text message. Per context you can send text, `flush`, and
`close_context` (with `"immediate": true` for
[barge-in](/streaming/barge-in#barge-in-on-multi-context-sessions));
`{"close_socket": true}` ends everything. The server tags every
context-specific response frame (`context_created`, `generation_started`,
`audio`, `chunk_complete`, `word_timestamps`, `final`, `context_closed`, and
per-context errors) with the originating `context_id`. The final
`session_closed` frame is connection-wide and has no `context_id`; it carries
`total_audio_seconds` across the connection. After each flush, a `final` frame
(ElevenLabs `is_final` equivalent) signals that all audio for the flushed text
has been delivered.

The full message tables — every field of every client→server and
server→client frame — live in the
[Text-to-Speech API reference](/api-reference/tts/multi-context).

Usage is billed **per context**: a synthesized, billable context's
`context_closed` frame carries its `usage` block. A context closed before
synthesis starts has no usage block. See
[Per-session usage](/streaming/turn-lifecycle#per-session-usage).

## Limits

* Maximum **20 concurrent contexts** per connection
* Contexts auto-close after **20 seconds** of inactivity
* Send empty text `{"text": "", "context_id": "..."}` to reset the
  per-context inactivity timeout
* Opening a context beyond the limit returns a per-context error
  (`error_code: "TOO_MANY_CONTEXTS"`, `code: 429`) without closing the
  connection — close an existing context, or wait for an idle one to be
  released, then retry.
