Jitter and Buffering: Smoothing Audio Streams in Voice Agents
Learn how jitter buffers work in voice AI systems and how to optimize buffer settings for low-latency, high-quality conversational experiences.
Published on • September 13, 2026
AI Assistant

Jitter and Buffering: Smoothing Audio Streams in Voice Agents
In real-time voice AI, audio packets don’t arrive at regular intervals. Network jitter creates timing variations that can destroy audio quality and break conversational flow. Jitter buffers solve this problem—but tuning them for AI agents requires a different approach than traditional VoIP.
The Jitter Problem
Why Packets Arrive Unevenly
Voice audio is transmitted in small packets (typically 20-60ms). Network conditions cause these packets to arrive with variable delays:
Sent: [1] [2] [3] [4] [5] [6] [7] [8] (even spacing)
Received: [1] [3] [2] [5] [4] [7] [6] [8] (jittered)
Without buffering, this creates:
- Audio gaps: Missing packets cause silence
- Timing errors: Out-of-order playback distorts speech
- ASR failures: Transcription models expect continuous audio
Jitter Buffer Basics
A jitter buffer holds packets briefly to reorder and smooth playback:
from collections import deque
import time
class SimpleJitterBuffer:
def __init__(self, target_delay_ms: float = 60.0,
max_delay_ms: float = 200.0):
self.target_delay_ms = target_delay_ms
self.max_delay_ms = max_delay_ms
self.buffer = deque()
self.last_playout_time = 0
def add_packet(self, packet: dict):
"""Add packet to buffer with timestamp."""
self.buffer.append({
'data': packet['data'],
'timestamp': packet['timestamp'],
'arrival_time': time.time() * 1000
})
# Sort by timestamp to handle reordering
self.buffer = deque(sorted(
self.buffer,
key=lambda x: x['timestamp']
))
def get_next_packet(self) -> dict:
"""Get next packet for playback when due."""
if not self.buffer:
return None
current_time = time.time() * 1000
packet = self.buffer[0]
# Check if packet is due for playout
playout_time = packet['arrival_time'] + self.target_delay_ms
if current_time >= playout_time:
self.buffer.popleft()
return packet
return None
Tuning Jitter Buffers for AI Agents
The AI Agent Difference
Traditional VoIP optimizes for human listening. AI voice agents have different requirements:
# Human-listening optimized (traditional)
human_config = {
'target_delay_ms': 100, # Higher delay OK for smoothness
'max_delay_ms': 300,
'min_delay_ms': 20
}
# AI-agent optimized
ai_config = {
'target_delay_ms': 40, # Lower delay for faster response
'max_delay_ms': 80, # Tighter bounds
'min_delay_ms': 10 # Aggressive minimum
}
Adaptive Jitter Buffer
Implement adaptive buffering based on network conditions:
class AdaptiveJitterBuffer:
def __init__(self):
self.target_delay_ms = 60.0
self.min_delay_ms = 20.0
self.max_delay_ms = 150.0
self.jitter_history = []
self.history_size = 100
def update_jitter_estimate(self, packet_interval_ms: float,
actual_interval_ms: float):
"""Update jitter estimate based on packet timing."""
jitter = abs(actual_interval_ms - packet_interval_ms)
self.jitter_history.append(jitter)
if len(self.jitter_history) > self.history_size:
self.jitter_history.pop(0)
# Calculate target delay based on 95th percentile jitter
if len(self.jitter_history) >= 10:
sorted_jitter = sorted(self.jitter_history)
p95_index = int(len(sorted_jitter) * 0.95)
p95_jitter = sorted_jitter[p95_index]
# Set target delay to 1.5x P95 jitter
self.target_delay_ms = max(
self.min_delay_ms,
min(self.max_delay_ms, p95_jitter * 1.5)
)
def get_target_delay(self) -> float:
"""Get current target delay."""
return self.target_delay_ms
Packet Loss Concealment
Handling Missing Packets
When packets are lost, concealment techniques fill the gaps:
class PacketLossConcealment:
def __init__(self, sample_rate: int = 16000):
self.sample_rate = sample_rate
self.last_packet = None
def conceal_loss(self, missing_samples: int) -> np.ndarray:
"""Generate concealment audio for missing samples."""
if self.last_packet is None:
# Silence if no previous packet
return np.zeros(missing_samples)
# Simple repetition-based concealment
# Repeat last packet's tail with fade-out
concealment_length = min(missing_samples, len(self.last_packet))
concealment = self.last_packet[-concealment_length:].copy()
# Apply fade-out to avoid clicks
fade_samples = min(100, concealment_length)
fade_out = np.linspace(1.0, 0.0, fade_samples)
concealment[-fade_samples:] *= fade_out
# Pad with silence if needed
if concealment_length < missing_samples:
padding = np.zeros(missing_samples - concealment_length)
concealment = np.concatenate([concealment, padding])
return concealment
def update_last_packet(self, packet: np.ndarray):
"""Update last received packet."""
self.last_packet = packet
Forward Error Correction (FEC)
Use codec-level FEC for proactive loss recovery:
class OpusFECDecoder:
"""Opus codec with in-band FEC for packet loss recovery."""
def __init__(self):
# Opus FEC embeds previous frame in current packet
self.previous_frame = None
def decode_with_fec(self, packet: bytes,
packet_lost: bool = False) -> np.ndarray:
"""Decode packet with FEC recovery."""
if packet_lost and self.previous_frame is not None:
# Use FEC data from previous packet
return self.decode_fec_from_previous()
# Normal decode
decoded = self.decode_opus(packet)
self.previous_frame = decoded
return decoded
def decode_fec_from_previous(self) -> np.ndarray:
"""Extract FEC data from previous packet."""
# FEC data is embedded in the Opus packet
# Implementation depends on Opus library
pass
Optimizing for Barge-In
The Barge-In Challenge
When users interrupt, buffered audio must be flushed immediately:
class BargeInHandler:
def __init__(self):
self.outbound_buffer = []
self.is_playing = False
def handle_barge_in(self):
"""Flush buffered audio on user interruption."""
# Clear outbound buffer
self.outbound_buffer.clear()
self.is_playing = False
# Clear playback device buffer
self.clear_playback_device()
# Reset TTS stream if active
self.reset_tts_stream()
def clear_playback_device(self):
"""Clear audio device buffer."""
# Platform-specific implementation
pass
Transport Layer Flush
Clean interruption requires flushing across the media path:
class WebRTCBargeInManager:
def __init__(self, peer_connection):
self.pc = peer_connection
self.outbound_track = None
async def interrupt_agent_speech(self):
"""Flush all queued audio across WebRTC transport."""
# Stop sending new audio
if self.outbound_track:
self.outbound_track.enabled = False
# Flush SFU-side buffers
await self.flush_sfu_buffers()
# Flush client-side playout buffer
await self.flush_client_buffer()
# Re-enable for new speech
if self.outbound_track:
self.outbound_track.enabled = True
async def flush_sfu_buffers(self):
"""Flush Selective Forwarding Unit buffers."""
# SFU-specific implementation
pass
Configuration by Network Type
Stable Broadband
broadband_config = {
'target_delay_ms': 30,
'max_delay_ms': 60,
'min_delay_ms': 10,
'plc_enabled': True,
'fec_enabled': True
}
Mobile/LTE
mobile_config = {
'target_delay_ms': 50,
'max_delay_ms': 100,
'min_delay_ms': 20,
'plc_enabled': True,
'fec_enabled': True,
'adaptive_enabled': True
}
Lossy Network
lossy_config = {
'target_delay_ms': 80,
'max_delay_ms': 150,
'min_delay_ms': 40,
'plc_enabled': True,
'fec_enabled': True,
'retransmission_enabled': True
}
Monitoring and Metrics
Key Metrics to Track
class JitterBufferMetrics:
def __init__(self):
self.metrics = {
'avg_jitter_ms': 0,
'max_jitter_ms': 0,
'packet_loss_rate': 0,
'buffer_overflow_count': 0,
'avg_buffer_level_ms': 0
}
def record_packet(self, jitter_ms: float, lost: bool):
"""Record packet statistics."""
self.metrics['avg_jitter_ms'] = (
self.metrics['avg_jitter_ms'] * 0.95 + jitter_ms * 0.05
)
self.metrics['max_jitter_ms'] = max(
self.metrics['max_jitter_ms'], jitter_ms
)
if lost:
self.metrics['packet_loss_rate'] = (
self.metrics['packet_loss_rate'] * 0.99 + 0.01
)
Alerting Thresholds
class JitterBufferAlerts:
def __init__(self):
self.thresholds = {
'high_jitter_ms': 100,
'high_loss_rate': 0.05,
'buffer_overflow_rate': 0.01
}
def check_alerts(self, metrics: dict) -> list:
"""Check for concerning conditions."""
alerts = []
if metrics['avg_jitter_ms'] > self.thresholds['high_jitter_ms']:
alerts.append('HIGH_JITTER')
if metrics['packet_loss_rate'] > self.thresholds['high_loss_rate']:
alerts.append('HIGH_PACKET_LOSS')
return alerts
Best Practices
1. Measure Before Tuning
# Establish baseline
baseline = measure_jitter_buffer_performance(
duration_seconds=300,
sample_rate=100 # ms between samples
)
print(f"P50 jitter: {baseline['p50_jitter_ms']}ms")
print(f"P95 jitter: {baseline['p95_jitter_ms']}ms")
print(f"Packet loss: {baseline['packet_loss_rate']*100}%")
2. Use Aggressive Defaults for AI
# Start with AI-optimized settings
config = {
'target_delay_ms': 40,
'max_delay_ms': 80,
'min_delay_ms': 15
}
# Adjust based on measured conditions
if measured_p95_jitter > 50:
config['max_delay_ms'] = 120
3. Implement Graceful Degradation
class GracefulJitterBuffer:
def __init__(self):
self.normal_config = {'target_delay_ms': 40}
self.degraded_config = {'target_delay_ms': 80}
self.current_config = self.normal_config
def handle_degradation(self, condition: str):
"""Switch to degraded mode on poor conditions."""
if condition == 'high_jitter':
self.current_config = self.degraded_config
elif condition == 'packet_loss':
self.current_config = self.degraded_config
4. Monitor End-to-End Latency
class EndToEndLatencyMonitor:
def __init__(self):
self.samples = []
def measure_turn_latency(self, user_speech_end: float,
agent_speech_start: float):
"""Measure time from user stop to agent start."""
latency_ms = (agent_speech_start - user_speech_end) * 1000
self.samples.append(latency_ms)
if len(self.samples) > 100:
self.samples.pop(0)
return latency_ms
Conclusion
Jitter buffers are essential for voice AI, but they require careful tuning:
- AI agents need lower latency than human listening
- Adaptive buffers respond to changing network conditions
- Packet loss concealment prevents audio gaps
- Barge-in requires flushing across the entire media path
- Monitor continuously to catch degradation early
The key insight: every millisecond of jitter buffer delay adds to the user’s perceived latency. For AI voice agents, where the total budget is often under 1000ms, buffer tuning can make the difference between a natural conversation and a frustrating experience.
Start with aggressive settings, measure real-world performance, and adjust based on your specific network conditions. The optimal configuration is always a balance between latency and audio quality.