Scaling Concurrent Voice Sessions

9 min read Module 8 of 9 Topic 23 of 25

What you'll learn

  • Explain why streaming API connections need pooling rather than per-call ad hoc connections
  • Design autoscaling and session affinity for WebSocket-based media servers
  • Handle provider rate limits with backpressure and queueing instead of dropped calls
  • Load test a voice pipeline in a way that predicts real concurrent-session behavior
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

Everything in this course so far has been validated one call at a time. Production means hundreds or thousands of those calls happening at once, and the failure modes at that scale are different in kind, not just degree, from anything a single-call test surfaces. This lesson is about the infrastructure layer that keeps per-call latency numbers from Module 7 holding steady as concurrency climbs.

Connection pooling for streaming APIs

Opening a fresh WebSocket to Nova-3 or your TTS provider for every call pays a real setup cost (TLS handshake, auth, provider session init) at the worst possible moment: exactly when a traffic spike needs capacity fastest. Pool and pre-warm connections instead.

# connection_pool.py - pre-warmed pool for the ASR provider
class ASRConnectionPool:
    def __init__(self, min_size=20, max_size=500):
        self.min_size = min_size
        self.max_size = max_size
        self._pool: list[Connection] = []

    async def warm(self):
        self._pool = [await open_nova3_stream() for _ in range(self.min_size)]

    async def acquire(self) -> Connection:
        if self._pool:
            return self._pool.pop()
        if self.active_count() < self.max_size:
            return await open_nova3_stream()
        raise PoolExhausted("at max concurrent ASR connections")

Autoscaling and session affinity for media servers

A live call’s WebSocket lives on one media server instance (running LiveKit or Pipecat) for its whole duration. Autoscaling on CPU or connection count adds capacity for new calls, but it cannot rebalance sessions already pinned to an overloaded instance, so capacity planning needs headroom below saturation, not just a reactive scale-out trigger.

flowchart TB
    LB["Load balancer /\nsession router"] --> M1["Media server 1\n(pinned sessions)"]
    LB --> M2["Media server 2\n(pinned sessions)"]
    LB --> M3["Media server N\n(new capacity,\nautoscaled)"]
    M1 --> ASR["ASR pool"]
    M2 --> ASR
    M3 --> ASR
    ASR --> LLM["LLM core"]
    LLM --> TTS["TTS pool"]

    style LB fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style M1 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style M2 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style M3 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style ASR fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style LLM fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style TTS fill:#fff7ed,stroke:#f59e0b,color:#0F172A

A practical rule: cap each media server instance at 60-70% of its measured max concurrent-session capacity, so a spike has room to land on existing instances while new ones spin up, typically 30-90 seconds behind demand.

Rate limits, backpressure, and queueing

Every upstream provider (ASR, LLM, TTS) has a rate limit, usually expressed as requests-per-second or concurrent-streams. Hitting it mid-call is worse than hitting it before a call starts, so apply backpressure at call admission, not mid-conversation.

async def admit_call(caller_id: str):
    if not await rate_limiter.try_acquire("llm_core", weight=1):
        # Queue briefly rather than drop; most callers tolerate
        # a short hold tone better than a dropped connection.
        await queue.enqueue(caller_id, max_wait_s=8)
        if not await rate_limiter.try_acquire("llm_core", weight=1):
            raise CapacityExceeded("route to overflow / voicemail")
    return await start_session(caller_id)

A short queue with a hold tone beats a dropped call; a hard reject with graceful overflow routing (voicemail or a callback offer) beats an unhandled timeout.

Load testing that reflects real traffic

A load test that opens N connections simultaneously with synchronized pre-recorded audio only tests instantaneous peak, not sustained load. Stagger call arrivals on a realistic distribution, vary turn timing and silence gaps, and run long enough (30+ minutes) to observe steady-state pool exhaustion, rate-limit backpressure, and autoscaling lag, not just the first burst.

# example: staggered load test driver (pseudocode via a load tool)
loadtest --concurrency 500 --arrival-distribution poisson \
  --mean-call-duration 240s --ramp-up 300s --duration 1800s \
  --target wss://voice-gateway.internal/session

Concurrency numbers from a synchronized burst test look better than production will actually behave; a staggered, sustained test is what predicts the p95 latency your Module 7 dashboards will actually show once real callers arrive.

Knowledge Check

3 questions to test your understanding

1 A team's voice pipeline opens a new WebSocket connection to the ASR provider for every incoming call, with no pooling. At 200 concurrent calls, they start seeing connection setup failures and elevated latency on the first turn of each call. What's the likely cause?

2 A media server cluster autoscales on CPU usage. During a traffic spike, new instances spin up, but calls already in progress on the original instances start degrading. What did the autoscaling policy miss?

3 A load test simulates 500 concurrent calls by opening 500 connections and sending pre-recorded audio simultaneously, then declares the system 'load tested.' What realistic behavior does this miss?

Discussion

Questions and notes from learners on this topic

Loading discussion…

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan