How to Distribute LiveKit Voice and Video Avatar Agents Across Pods with Kubernetes

Learn how to run LiveKit voice/video avatar agents on Kubernetes with sticky session routing, graceful shutdown, and pod scaling.
Introduction
When you put a live voice or video avatar in front of a real agent, the hard part is usually not the model. It is the plumbing: keeping sessions sticky, making sure media state follows the right process, and avoiding the classic failure mode where a websocket dies and the user gets dropped into silence.
This post is about distributing realtime avatar agents across Kubernetes pods without breaking the conversational experience. By the end, you should be able to reason about where session state lives, how to route LiveKit-based agents safely, and how to scale pods up and down without orphaning active calls.
We will assume a common setup: a voice agent that runs in a pod, connects to LiveKit for realtime media transport, and renders a synchronized avatar video layer on top of the agent’s speech. The same operational patterns apply whether the agent is customer support, an NPC, or a sales assistant. The only thing that changes is how much latency, concurrency, and failure isolation you need.
What actually needs to stay “sticky”
In Kubernetes, people often say “make it stateless” and move on. For avatar agents, that advice is directionally correct but incomplete. The worker pod can be stateless only if the long-lived realtime session is anchored somewhere else and the pod can be recreated without losing the session.
For a voice-and-avatar agent, there are usually three distinct pieces of state:
Transport state: the LiveKit room connection, tracks, and websocket/media session.
Conversation state: transcript, turn-taking state, tool results, and any memory you keep in your app.
Avatar/session state: the mapping between the agent’s speech stream and the synchronized face/video output.
The transport state is inherently tied to a process until the realtime connection is re-established. The conversation and avatar metadata should usually be externalized so any pod can resume work or take over a new session.
The practical consequence: do not route a user’s realtime session to arbitrary pods mid-call unless your agent stack explicitly supports session migration. Instead, design for session affinity at the application layer, not just at the load balancer layer.
Use Kubernetes for horizontal concurrency, not in-process persistence
The cleanest deployment model is one agent worker per active session, with pods acting as interchangeable executors. Kubernetes then becomes a scheduler for concurrency, not a database for live call state.
A typical flow looks like this:
A control plane creates or receives a session request.
The request is assigned to a worker pod.
The worker joins the LiveKit room, starts the agent loop, and attaches the avatar video output.
All session metadata needed for recovery is written externally.
If the pod dies, the session is either terminated cleanly or recreated as a new session on another pod.
This is the important architectural choice: you are not trying to move a live websocket between pods. You are making sure that a pod crash does not corrupt shared state, and that a new pod can start the next turn with enough context.
Practical routing patterns that work
There are three patterns I see most often.
1. One pod owns one session
This is the simplest model and the one I would start with. Each worker process handles exactly one active avatar session at a time. If you need more throughput, scale replicas horizontally.
Pros:
Easy to reason about failure domains.
No intra-cluster session handoff.
Debugging is straightforward because logs and media state line up with one pod.
Cons:
Potentially lower utilization if your sessions are bursty.
You need an external dispatcher to place work on pods.
2. Worker pool with admission control
In this model, each pod advertises capacity, usually “N concurrent sessions.” A dispatcher sends new sessions to pods that still have room. This is a good fit if your agent has a fixed amount of CPU/GPU per call and you want predictable utilization.
You still keep the live session process-local. The only difference is that the pod can accept several independent sessions, each isolated by its own task or thread.
3. Sticky routing by session ID
If you have an API tier in front of the workers, route all requests for a given session ID to the same pod. This is useful when the agent has a chatty control path—tool calls, pause/resume, instruction updates—but the underlying realtime media connection still stays on the pod that owns the session.
Sticky routing is a routing concern, not a recovery mechanism. If the pod disappears, the sticky rule just keeps pointing at a dead endpoint. So pair it with durable session metadata and a reconnection story.
How to make the pod lifecycle boring
The main operational goal is to avoid pod eviction in the middle of a live call, and when that is impossible, make the failure obvious and fast.
Use these baseline practices:
Set realistic terminationGracePeriodSeconds so a worker can close LiveKit connections cleanly.
Expose readiness only after the worker is actually able to accept new sessions.
Stop routing new work on shutdown before terminating active sessions.
Write session checkpoints externally so a replacement pod can resume from the last committed conversation state.
Avoid relying on pod IPs for anything session-related; use stable IDs and an external registry.
If you use HPA, scale on something meaningful: active sessions, queue depth, CPU, or a custom metric tied to realtime load. Scaling on CPU alone can lag badly for media-heavy or model-heavy agents.
Minimal worker example with the LiveKit plugin
If your agent runs inside a LiveKit voice worker, the avatar layer is usually just another plugin in the agent pipeline. The exact integration points vary by framework, but the shape is the same: initialize the avatar client, bind it to the agent’s speech output, and let the plugin handle synchronized rendering.
The important part is not the specific method name; it is the lifecycle. The avatar object should be created per session, attached to that session’s media pipeline, and torn down when the session ends. Do not share a single avatar instance across pods or across unrelated calls.
If you are using Pipecat instead of a LiveKit agent worker, the same principle applies. The integration guide and plugin repo are useful references for where the avatar sits in the media graph: Pipecat integration guide and plugin repository.
Session orchestration: keep control plane and media plane separate
One common mistake is to let the worker pod both create sessions and own the realtime media connection, with no external coordination. That works until you need retries, auditability, or a second pod to take over.
A better separation is:
Control plane: creates session records, assigns workers, stores instructions and metadata, and issues API calls.
Media plane: the worker pod that joins LiveKit and runs the realtime agent loop.
That separation gives you a clean place to retry failed startup, rate limit session creation, and observe what is actually happening in production. It also makes it much easier to reason about whether an outage is in your orchestration layer or in the media/session layer.
If you need a simple external registry, the Protoface REST API is a reasonable place to create and manage avatar sessions from the control plane, while the worker pods focus on realtime execution. A minimal request shape looks like this:
Exact endpoints and fields are documented in the docs. The point here is the operational pattern: keep secrets out of the browser, create session state in a durable place, and let workers join/own the live media connection only for the lifetime of one call.
Failure modes you should design for
Most production issues fall into a small number of buckets:
Pod eviction or node drain: terminate cleanly and mark the session ended; do not pretend the call is still live.
Transient network loss: if the LiveKit connection drops briefly, the worker may reconnect, but only if your agent framework supports it and the session is still valid.
Backpressure: too many concurrent sessions on one pod will increase end-to-end latency and degrade lip sync.
Split-brain routing: two pods think they own the same session; prevent this with a single authoritative assignment record.
For the last one, use a compare-and-swap style update, a lease, or another distributed lock primitive when assigning a session to a pod. If a worker crashes and the lease expires, another pod can claim the session cleanly.
Also pay attention to shutdown ordering. Stop accepting new sessions, drain existing work, close the media connection, then exit. If you reverse those steps, you will create intermittent truncation and hard-to-reproduce state bugs.
Where Protoface fits
For teams that want the avatar layer handled as a first-class piece of the stack, Protoface exposes the pieces you need without forcing you to expose secrets in the browser. In this Kubernetes pattern, the most relevant surface is the LiveKit agent plugin: the worker pod owns the realtime session, the plugin adds the synchronized face/video output, and your control plane manages the session lifecycle. That keeps the deployment model aligned with how Kubernetes actually works: replace pods, not live calls.
Conclusion
Distributing voice and video avatar agents across Kubernetes is mostly about respecting session boundaries. Keep the realtime transport attached to one worker pod, externalize the control state, and use Kubernetes for scaling and failure recovery rather than for live session migration.
If you get those boundaries right, the rest is standard operational work: readiness, graceful shutdown, lease-based assignment, and metrics that reflect real concurrency. For implementation details, start with the documentation and the relevant quickstarts in the Protoface GitHub org, then test pod termination and rescheduling before you ship.
