· — Dishant Sethi ·Jul 6, 2026·13 min read

Self-Hosting LiveKit at Scale: Architecture from 90K+ Calls/Month

The complete production architecture for self-hosting LiveKit — standalone server, Python agent workers, LiveKit Egress on ECS, and multi-metric autoscaling from a team running 90K+ calls/month with five selectable AI pipelines.

Key Takeaways

  • Self-hosting the complete LiveKit stack — server, agent workers, and Egress — removes every LiveKit Cloud tier constraint; the only ceiling is your own ECS CPU and memory
  • The three-component architecture (LiveKit server for WebRTC signalling, Python agent workers for AI pipeline logic, LiveKit Egress for recording) gives each component an independent scaling axis
  • A factory pattern for pipeline selection lets you run Deepgram, Azure OpenAI, ElevenLabs, Claude Sonnet, and Sarvam variants off a single agent worker codebase — pipeline is a runtime configuration value, not a code branch
  • Multi-metric ECS autoscaling on active connections, CPU, memory, and queue depth handles 10x traffic spikes without proportional cost increase
  • Full migration from a Django WebSocket monolith to a self-hosted LiveKit stack takes approximately 12 weeks end-to-end

LiveKit Cloud hits a per-tier limit on concurrent agent sessions and egress minutes. For a low-traffic prototype that ceiling is invisible. For a production platform at 90K+ calls/month, it becomes a hard operational constraint — both on capacity and on cost-per-call.

Self-hosting the complete LiveKit stack — server, agent workers, and egress — solves both problems. The catch is that "self-host LiveKit" is ambiguous: LiveKit is three separate components, not one binary, and each scales independently. Getting this right requires understanding what each component actually does.

Self-hosted LiveKit production guide — the short answer. A production self-hosted LiveKit deployment consists of three ECS services: the LiveKit server (WebRTC signalling and media routing), Python agent workers (the VAD → STT → LLM → TTS pipeline logic), and LiveKit Egress (recording to S3). A Django or similar control plane handles room creation, JWT token generation, and session lifecycle. The only hard ceiling is your own infrastructure capacity.

Why LiveKit Cloud Hits a Ceiling

LiveKit Cloud imposes per-plan limits on concurrent agent sessions, egress minutes, and active agent deployments. At low scale these limits are invisible; at 90K+ calls/month they become a daily operational concern — both as capacity ceilings and as per-minute billing that compounds with call volume at every scale-up.

The cost model also changes at scale. LiveKit Cloud bills per participant-minute and per egress minute. Self-hosting replaces that with ECS CPU and memory costs, which scale linearly with load and can be optimized with reserved capacity and scheduled scaling. For teams with predictable call volume and internal DevOps capability, self-hosting produces meaningfully lower per-call costs beyond approximately 30K calls/month.

Beyond cost, self-hosting gives you:

  • No concurrency ceilings — add ECS tasks to handle any peak; no plan upgrade required
  • Full infrastructure control — STUN, TURN, media routing, and recording all within your own VPC
  • HIPAA/SOC2 scope clarity — easier to scope a BAA when all media traverses your own infrastructure
  • Egress flexibility — record directly to your S3 bucket; no third-party data custody on call recordings

The Self-Hosted LiveKit Stack: Three Independent Services

A self-hosted LiveKit production deployment runs as three distinct ECS services — not one binary. Most guides conflate them; understanding the separation is prerequisite to deploying any of them correctly, and to autoscaling each component independently based on its own failure mode and resource profile.

LiveKit Server handles WebRTC signalling, ICE negotiation, DTLS/SRTP termination, and media routing. It is the Selective Forwarding Unit (SFU): it receives encoded audio from browser clients, manages room state, and forwards RTP packets to agent workers. The server contains no AI logic. It runs as one ECS service, autoscaling on active connections and connection establishment rate.

Agent Workers run your AI pipeline code — the VAD → STT → LLM → TTS chain that processes each call. Workers connect to the LiveKit server as a special participant type, receive RTP audio frames, run the pipeline, and publish synthesized audio back into the room. They scale independently of the server, on CPU and memory. This is the architectural win: voice AI processing latency cannot affect WebRTC signalling stability when both run on separate ECS services.

LiveKit Egress handles call recording — it subscribes to room tracks and outputs them as MP4 or audio files to S3. It runs as a third ECS service, only active when recording is requested. Egress is commonly omitted from initial self-hosted deployments and added as a retrofit; build it in from the start to avoid a second migration.

Deploying the LiveKit Server on ECS

The LiveKit server is a stateless Go binary distributed as livekit/livekit-server. It requires a YAML configuration file, a Redis instance for distributed room state across replicas, and specific UDP port range exposure in your security groups — the last of which is the most common production deployment failure in new self-hosted LiveKit setups.

# livekit.yaml
port: 7880
rtc:
  port_range_start: 50000
  port_range_end: 60000
  use_external_ip: true
redis:
  address: <elasticache-endpoint>:6379
keys:
  <api-key>: <api-secret>
turn:
  enabled: true
  domain: turn.yourdomain.com
  tls_port: 5349
  udp_port: 443
  external_tls: true

Key infrastructure requirements that trip up most deployments:

  • Redis (ElastiCache) — required for room state synchronization across 2+ server instances; without it, multi-instance deployments produce split-brain room state silently
  • UDP port range — ports 50000–60000 must be open in your ECS security group and NAT gateway; the most common cause of WebRTC ICE failures in new self-hosted setups
  • STUN/TURN — coturn or LiveKit's built-in TURN for clients behind symmetric NAT, common in enterprise and mobile networks
  • Health endpoint/healthz on port 7880; configure ECS health checks against it with a 45-second startup grace period

The ECS task definition needs both TCP 7880 (HTTP/WebSocket signalling) and the full UDP port range exposed. Security groups must permit UDP from 0.0.0.0/0 in that range — client IP ranges are unpredictable in WebRTC and cannot be scoped to a CIDR.

Building Production Python Agent Workers

The livekit-agents Python SDK provides the framework for building agent workers. A production worker is an asyncio application that connects to the LiveKit server as a special participant type, receives job dispatch events for each call, runs the AI pipeline, and publishes synthesized audio back into the room — all without blocking concurrent calls.

The production pattern Prodinit uses routes pipeline selection through a JSON metadata field sent by the control plane at dispatch time:

from livekit import agents
from livekit.agents import JobContext, WorkerOptions
import json

async def entrypoint(ctx: JobContext):
    await ctx.connect()
    metadata = json.loads(ctx.room.metadata or "{}")
    pipeline_type = metadata.get("simulation_service", "default")
    pipeline = PipelineFactory.create(pipeline_type)
    await pipeline.run(ctx)

if __name__ == "__main__":
    agents.cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))

Each pipeline implements a common interface — run(ctx: JobContext) — with a different STT/LLM/TTS combination behind it. Switching a session to a different pipeline is a metadata value change in Django's dispatch call; no deployment required.

The VAD → STT → LLM → TTS chain inside each pipeline handles:

  • VAD — Silero VAD with a 300ms trailing silence window; configured per pipeline depending on expected conversation tempo
  • STT — Deepgram Nova-3 via WebSocket streaming; is_final: true events trigger LLM dispatch; endpointing=300 set explicitly
  • LLM — streamed completion; first tokens forwarded to TTS before generation completes — this overlap recovers 100–200ms of total latency
  • TTS — ElevenLabs Flash (eleven_flash_v2_5) or Azure Speech Service, streaming output; first audio chunk delivered to the LiveKit room within 60–100ms of the TTS call
  • Stopword detection — async check on every utterance detects interruption keywords and halts current playback immediately
  • Max-duration enforcement — per-session timeout kills the call and triggers post-call cleanup if maximum duration is exceeded

Post-call cleanup runs in a background thread: MP4 assembly via imageio-ffmpeg at 15fps, S3 upload, transcript POST to Django, and session cleanup POST. Background execution means the agent is available for the next call dispatch immediately.

Factory Pattern for Multiple AI Pipelines

Running multiple voice AI pipeline variants off a single agent worker codebase is the requirement that distinguishes a production self-hosted deployment from a tutorial example. In the platform Prodinit built for a client, five distinct pipelines operate concurrently at production scale:

  • Deepgram Nova-3 → Azure OpenAI GPT-4o Realtime → ElevenLabs TTS
  • Deepgram Nova-3 → Claude Sonnet → ElevenLabs TTS
  • Deepgram Nova-3 → Azure OpenAI → Azure Speech TTS
  • Google Chirp STT → Gemini 2.0 Flash → ElevenLabs TTS
  • Sarvam STT → Gemini Chat → ElevenLabs TTS (Hindi and Indian language sessions)

The factory pattern keeps these as separate classes under a common interface:

class PipelineFactory:
    _registry = {
        "azure_openai": AzureOpenAIPipeline,
        "claude":       ClaudeSonnetPipeline,
        "azure_speech": AzureSpeechPipeline,
        "gemini_live":  GeminiLivePipeline,
        "sarvam":       SarvamPipeline,
    }

    @classmethod
    def create(cls, pipeline_type: str) -> BasePipeline:
        klass = cls._registry.get(pipeline_type)
        if klass is None:
            raise ValueError(f"Unknown pipeline: {pipeline_type}")
        return klass()

Registering a new pipeline variant requires no changes to the dispatch logic or the worker entrypoint — add the class, add the registry entry, deploy. This pattern also enables A/B testing across pipeline variants: route a percentage of sessions to an experimental pipeline via the metadata field in Django's dispatch call, with no agent code change required.

Multi-Metric ECS Autoscaling

Single-metric autoscaling is the most common operational failure mode in self-hosted LiveKit deployments. CPU-only scaling misses memory pressure from concurrent pipeline state; connection-count-only scaling misses agent worker queue build-up. Multi-metric policies across all three ECS services are what enabled 10x peak load capacity for a client deployment without proportional cost increase.

LiveKit Server scaling:

  • Scale-up trigger: active WebSocket connections > 80% of current capacity, or connection establishment rate > 50/sec
  • Scale-down: gradual, 30-minute cooldown to prevent flapping between call bursts
  • Minimum 2 tasks always running for high availability

Agent Workers scaling:

  • Scale-up trigger: CPU > 60%, memory > 70%, or pending dispatch jobs > 5
  • The pending dispatch jobs metric is the most important: it catches queue build-up before CPU or memory signals appear
  • Scale-down: 15-minute cooldown; memory clears slowly as concurrent pipeline state is released after calls end

Celery Workers (post-call processing — transcript generation, S3 upload, metadata sync):

  • Scale on queue depth per task type and task wait time (p90 > 10 seconds triggers scale-out)
  • Separate pools: high-memory for voice AI tasks, I/O-optimized for notification tasks, CPU-optimized for data processing tasks

Scheduled scaling — pre-warming ECS tasks 15 minutes before known peak hours — is non-optional for voice AI platforms with predictable daily traffic patterns. ECS cold-start for a new agent worker task (container pull plus SDK initialization) is 45–90 seconds. That startup latency surfaces as elevated call drop rates during traffic spikes without pre-warming.

Operational Instrumentation

Self-hosting LiveKit introduces four failure surfaces that don't exist with the managed tier: the LiveKit server process, TURN server availability, agent worker dispatch queue, and Egress recording. All four fail silently without dedicated monitoring — meaning production incidents appear as dropped calls or missing recordings rather than explicit errors.

Prodinit adds the following instrumentation on every self-hosted LiveKit deployment:

Sentry on agent workers — uncaught exceptions in pipeline code manifest as silent call drops without error tracking. Configure before_send to strip PII from exception payloads before they leave the VPC.

LiveKit metrics endpoint (/metrics on port 7880) scraped by Prometheus — room count, participant count, active publisher tracks, and RTP packet loss rates. Alert on packet loss > 1%; it's the earliest signal of UDP port range or TURN configuration problems before callers notice audio degradation.

Per-call structured logging with call_id, pipeline_type, stt_latency_ms, llm_first_token_ms, tts_first_chunk_ms, and total_latency_ms on every completed call. Without a consistent call_id on every log event, joining logs across Deepgram, the agent worker, and the LiveKit server for a single incident is infeasible.

ECS task replacement alerting — an agent worker that crashes mid-call disconnects the participant without any application-level error. Alert on ECS taskStopped events with stopCode: EssentialContainerExited; a rate above 0.5% of active tasks indicates systemic worker instability requiring investigation.

Redis health — the LiveKit server silently falls back to single-instance mode if Redis connectivity is lost. Add a CloudWatch alarm on ElastiCache connection count; a drop to zero means your LiveKit server instances have lost distributed state synchronization and room state is no longer shared across replicas.

For a broader treatment of WebRTC transport, latency budgeting, and voice AI observability patterns, see our production voice AI architecture guide. For connecting a self-hosted LiveKit server to the phone network via SIP trunking, see our LiveKit Cloudonix SIP trunking guide.

Get Prodinit's AI engineering guides in your inbox

Deep-dives on production LLMs, voice AI, and MLOps — published weekly. No sales emails.

Frequently Asked Questions

LiveKit Cloud imposes per-plan limits on concurrent agent sessions, egress minutes, and agent deployments. Self-hosting removes all ceilings — the only constraint is ECS capacity. Beyond approximately 30K calls/month, per-minute billing is replaced with EC2 and ElastiCache costs at meaningfully lower per-call economics. For HIPAA environments, self-hosting keeps all media within your own VPC without third-party data custody.

Scale ECS agent workers on CPU (>60%), memory (>70%), and pending dispatch job count from the LiveKit server simultaneously. CPU-only scaling misses concurrent pipeline state held in memory. Pending job count is the most actionable signal — it detects queue build-up before latency degrades. Use a 15-minute scale-down cooldown; pipeline memory clears slowly after calls end.

Minimum stack: LiveKit server ECS service with UDP ports 50000–60000 open in security groups, ElastiCache Redis for distributed room state across server replicas, coturn or built-in TURN for NAT traversal, and separate ECS services for agent workers and Egress. Redis is the most commonly missed dependency — without it, multiple LiveKit server instances produce split-brain room state silently.

A complete self-hosted LiveKit migration — server and agent infrastructure, 5 AI pipeline variants, Django control plane refactor, ECS autoscaling, and Egress setup — takes approximately 12 weeks based on Prodinit's engagement with a client. The highest-risk phase is the parallel rollout where the old path and the new self-hosted path run simultaneously with a traffic split until confidence in the new stack is established.

LiveKit Egress records room audio tracks as MP4 files directly to an S3 bucket you specify, running as a separate ECS service triggered by an API call from your control plane. Post-call processing — imageio-ffmpeg MP4 assembly, S3 upload, and transcript POST back to Django — runs in a background thread on the agent worker so it does not block the next call dispatch.

Stay ahead in AI engineering.

Get the latest insights on building production AI systems, be the first to explore approaches that actually work beyond the demo.

Start a Project →