Streaming Token Delivery for Voice Interfaces
Learn how streaming token delivery reduces latency in voice AI by sending LLM tokens directly to TTS engines as they are generated.
Published on • September 13, 2026
AI Assistant

Streaming Token Delivery for Voice Interfaces
In real-time voice AI, waiting for complete responses before speaking feels unnatural. Streaming token delivery solves this by sending LLM tokens directly to TTS engines as they’re generated—enabling immediate audio playback while the model is still thinking.
The Latency Problem
Sequential Pipeline (Slow)
User speaks → STT processes → LLM generates complete response → TTS synthesizes → Audio plays
This creates dead air while each stage completes:
# Sequential processing (high latency)
async def sequential_pipeline(audio: bytes):
# STT: 200ms
transcript = await stt.transcribe(audio)
# LLM: 500ms
response = await llm.generate(transcript)
# TTS: 300ms
audio = await tts.synthesize(response)
# Total: ~1000ms delay
return audio
Streaming Pipeline (Fast)
User speaks → STT streams → LLM streams tokens → TTS streams audio → Audio plays immediately
Each stage starts before the previous finishes:
# Streaming pipeline (low latency)
async def streaming_pipeline(audio_stream):
# Stream STT
async for transcript_chunk in stt.stream_transcribe(audio_stream):
# Stream LLM tokens
async for token in llm.stream_generate(transcript_chunk):
# Stream TTS audio
audio_chunk = await tts.stream_synthesize(token)
yield audio_chunk # Play immediately
# Total: ~200ms to first audio byte
Architecture Components
1. LLM Token Streaming
class StreamingLLM:
def __init__(self, model: str):
self.model = model
async def stream_tokens(self, prompt: str) -> AsyncGenerator[str, None]:
"""Stream tokens as they're generated."""
response = await self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
stream=True
)
async for chunk in response:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
2. Intelligent Chunking
class TokenChunker:
def __init__(self, min_chunk_size: int = 20,
max_chunk_size: int = 100):
self.min_chunk_size = min_chunk_size
self.max_chunk_size = max_chunk_size
self.buffer = ""
def add_token(self, token: str) -> Optional[str]:
"""Add token and return chunk when ready."""
self.buffer += token
# Check for sentence boundary
if self._is_sentence_boundary():
chunk = self.buffer
self.buffer = ""
return chunk
# Check for minimum size
if len(self.buffer) >= self.max_chunk_size:
chunk = self.buffer
self.buffer = ""
return chunk
return None
def _is_sentence_boundary(self) -> bool:
"""Check if buffer ends at sentence boundary."""
boundaries = ['.', '!', '?', ',', ';', ':']
return any(self.buffer.endswith(b) for b in boundaries)
3. WebSocket Transport
import websockets
import json
class StreamingTTSClient:
def __init__(self, tts_url: str):
self.url = tts_url
self.ws = None
async def connect(self):
"""Establish WebSocket connection."""
self.ws = await websockets.connect(self.url)
async def stream_text(self, text_stream):
"""Stream text tokens to TTS."""
async for chunk in text_stream:
await self.ws.send(json.dumps({
"text": chunk,
"voice": "default"
}))
async def receive_audio(self) -> AsyncGenerator[bytes, None]:
"""Receive audio chunks from TTS."""
async for message in self.ws:
data = json.loads(message)
if "audio" in data:
yield bytes(data["audio"])
Complete Streaming Pipeline
class StreamingVoicePipeline:
def __init__(self):
self.stt = StreamingSTT()
self.llm = StreamingLLM()
self.tts = StreamingTTS()
self.chunker = TokenChunker()
async def process_stream(self, audio_input):
"""End-to-end streaming pipeline."""
# Stream STT
transcript_stream = self.stt.stream_transcribe(audio_input)
# Stream LLM tokens
token_stream = self.llm.stream_tokens(transcript_stream)
# Chunk tokens for TTS
async for token in token_stream:
chunk = self.chunker.add_token(token)
if chunk:
# Stream chunk to TTS
audio_chunk = await self.tts.synthesize_chunk(chunk)
# Play immediately
yield audio_chunk
# Flush remaining buffer
if self.chunker.buffer:
audio_chunk = await self.tts.synthesize_chunk(
self.chunker.buffer
)
yield audio_chunk
Handling Interruptions
Barge-In Support
class InterruptibleStreamingPipeline:
def __init__(self):
self.is_playing = False
self.current_tts_task = None
async def handle_interruption(self):
"""Handle user interruption during playback."""
if self.current_tts_task:
# Cancel current TTS generation
self.current_tts_task.cancel()
# Clear audio buffer
self.clear_playback_buffer()
# Reset pipeline state
self.reset_state()
async def stream_with_interruption(self, audio_stream):
"""Stream with interruption support."""
try:
async for chunk in self.process_stream(audio_stream):
self.is_playing = True
yield chunk
except asyncio.CancelledError:
# Interruption occurred
self.is_playing = False
return
Latency Optimization Techniques
1. Connection Persistence
class PersistentConnectionManager:
def __init__(self):
self.connections = {}
async def get_connection(self, service: str):
"""Get or create persistent connection."""
if service not in self.connections or \
self.connections[service].closed:
self.connections[service] = await self.create_connection(service)
return self.connections[service]
async def create_connection(self, service: str):
"""Create new WebSocket connection."""
return await websockets.connect(
self.get_service_url(service)
)
2. Token Buffering
class AdaptiveBuffer:
def __init__(self, target_latency_ms: int = 200):
self.target_latency = target_latency_ms
self.buffer = []
self.last_flush_time = 0
def add_token(self, token: str) -> Optional[list]:
"""Add token and return buffer if ready."""
self.buffer.append(token)
current_time = time.time() * 1000
time_since_flush = current_time - self.last_flush_time
# Flush if buffer is large enough or timeout
if len(self.buffer) >= 10 or \
time_since_flush > self.target_latency:
chunk = self.buffer.copy()
self.buffer.clear()
self.last_flush_time = current_time
return chunk
return None
3. Predictive Prefetching
class PredictivePrefetch:
def __init__(self):
self.prediction_model = None
async def predict_next_tokens(self, current_tokens: list):
"""Predict likely next tokens for prefetching."""
# Use small model to predict next few tokens
prediction = await self.prediction_model.predict(
current_tokens[-10:] # Last 10 tokens
)
return prediction.get('next_tokens', [])
Monitoring and Metrics
class StreamingMetrics:
def __init__(self):
self.metrics = {
'time_to_first_byte_ms': [],
'chunk_to_chunk_ms': [],
'total_pipeline_latency_ms': [],
'interruption_recovery_ms': []
}
def record_streaming_performance(self, ttfa_ms: float,
chunk_latency_ms: float):
"""Record streaming metrics."""
self.metrics['time_to_first_byte_ms'].append(ttfa_ms)
self.metrics['chunk_to_chunk_ms'].append(chunk_latency_ms)
def get_percentiles(self, metric: str) -> dict:
"""Calculate percentiles for a metric."""
values = sorted(self.metrics[metric])
n = len(values)
return {
'p50': values[int(n * 0.5)],
'p95': values[int(n * 0.95)],
'p99': values[int(n * 0.99)]
}
Best Practices
1. Optimize Chunk Sizes
# Too small: High overhead, potential gaps
chunk_size = 10 # tokens
# Too large: High latency
chunk_size = 100 # tokens
# Sweet spot: Balance latency and naturalness
chunk_size = 30 # tokens (or sentence boundaries)
2. Handle Network Jitter
class JitterBuffer:
def __init__(self, buffer_ms: int = 200):
self.buffer_size = buffer_ms
self.audio_buffer = []
def add_chunk(self, chunk: bytes, timestamp: float):
"""Add audio chunk to jitter buffer."""
self.audio_buffer.append({
'data': chunk,
'timestamp': timestamp
})
# Sort by timestamp
self.audio_buffer.sort(key=lambda x: x['timestamp'])
def get_playable_chunk(self) -> Optional[bytes]:
"""Get next chunk when ready for playback."""
if not self.audio_buffer:
return None
current_time = time.time() * 1000
next_chunk = self.audio_buffer[0]
if current_time >= next_chunk['timestamp'] + self.buffer_size:
self.audio_buffer.pop(0)
return next_chunk['data']
return None
3. Monitor Real Performance
# Don't just measure TTS in isolation
# Measure end-to-end pipeline
async def measure_real_latency(pipeline, test_audio):
"""Measure actual user-perceived latency."""
start_time = time.time()
first_audio_received = False
async for chunk in pipeline.process_stream(test_audio):
if not first_audio_received:
ttfa_ms = (time.time() - start_time) * 1000
print(f"Time to first audio: {ttfa_ms}ms")
first_audio_received = True
total_time = (time.time() - start_time) * 1000
print(f"Total pipeline time: {total_time}ms")
Conclusion
Streaming token delivery transforms voice AI from “thinking” to “talking”:
- Stream everything: STT → LLM → TTS should all stream
- Chunk intelligently: Balance latency with natural prosody
- Handle interruptions: Users should be able to barge in
- Use persistent connections: Avoid reconnection overhead
- Monitor end-to-end: Isolate component metrics don’t show user experience
The key insight: the biggest latency win isn’t faster models—it’s eliminating sequential waiting. By streaming tokens directly from LLM to TTS, you can achieve sub-300ms time-to-first-audio, making voice agents feel truly conversational.