Header Logo

Load Testing WebSocket Signaling for Streaming Lip-Sync Avatars with k6

Load Testing WebSocket Signaling for Streaming Lip-Sync Avatars with k6

Load testing WebSocket signaling for lip-sync avatars with k6: measure handshake latency, round-trip time, reconnects, and stability.

Introduction


When you add a realtime avatar to a voice agent, the hard part is often not the model or the video pipeline. It’s the signaling path: the WebSocket that coordinates session setup, turn-taking, media timing, and state updates between your app and the service producing the avatar stream. If that channel stalls, reconnects too slowly, or falls over under concurrency, users see laggy faces, dropped sessions, or avatars that stop matching the audio.


This post shows how to load test that signaling layer with k6 in a way that is actually useful for developers shipping streaming lip-sync avatars. By the end, you should be able to design a test that measures handshake latency, sustained connection stability, message throughput, and reconnect behavior under realistic load, then interpret the results without confusing signaling bottlenecks with media-plane issues.


What you are really load testing


For streaming avatars, the WebSocket is usually a control plane, not the media plane. The video itself may arrive over WebRTC, a streamed asset pipeline, or an iframe-managed session, but the WebSocket still has to do the work that makes the stream coherent:


  • authenticate a session

  • create or resume an avatar conversation

  • exchange turn state and timing events

  • deliver partial text, audio timing, or viseme/lip-sync markers

  • handle reconnects and cleanup


That means “load testing the WebSocket” is not just opening lots of sockets and seeing whether they stay open. A good test answers a few specific questions:


  • How long does connection establishment take at different concurrency levels?

  • How many active sessions can the signaling tier sustain before latency rises sharply?

  • Do message round trips stay bounded when clients send bursts?

  • How does the system behave when clients disconnect mid-session and reconnect?


The mistake I see most often is using a generic chat-socket test and assuming it applies to streaming media. Lip-sync avatars are more timing-sensitive than chat. The backend can be “up” while still being unusable if message latency drifts enough to break the audio/video sync envelope.


Model the session shape before you write the test


Start by writing down the lifecycle of one avatar session. You do not need the exact server schema to do this; you need the sequence of states your client expects.


  1. Open WebSocket.

  2. Authenticate or attach a session token.

  3. Create or join an avatar session.

  4. Send a small set of control messages, such as “start speaking” or “update prompt.”

  5. Receive server acknowledgements and timing events.

  6. Keep the session alive with pings or heartbeat traffic.

  7. Close cleanly, or simulate abrupt disconnects.


That sequence matters because different failure modes show up in different phases. If the server has a token verification bottleneck, connection time spikes first. If the problem is per-session state management, you may see normal connects followed by slow message processing after a few minutes. If heartbeats or idle timeouts are misconfigured, sessions disappear under sustained load even though initial handshakes look fine.


Design k6 scenarios around realistic traffic


k6 is a good fit because it can generate many concurrent virtual users and keep each one stateful. For websocket tests, think in terms of scenario design rather than raw request count:


  • Ramp-up tests whether the signaling service degrades gracefully as concurrency climbs.

  • Steady-state tests expose memory growth, GC pressure, and backend saturation.

  • Burst tests catch issues in auth, queueing, and connection limits.

  • Reconnect tests exercise cleanup and session resumption logic.


Use thresholds that describe the user experience, not just infrastructure health. For example, you may care that p95 connection setup stays below a certain number of milliseconds, or that message round-trip time stays below a narrow bound while 500 sessions are active. Pick thresholds that reflect your application’s tolerance for lag. A conversational avatar can often tolerate a little control-plane delay, but not enough to desynchronize speech and motion.


Minimal k6 websocket skeleton


The exact WebSocket messages will depend on your signaling protocol, but the test structure is usually the same. This example is intentionally generic: it opens a socket, performs a small handshake, sends a control message, waits for a response, and then closes.


import ws from 'k6/ws';

}
import ws from 'k6/ws';

}
import ws from 'k6/ws';

}


A few practical notes:


  • Use separate VU identities if your backend associates one socket with one user or one avatar.

  • Do not send heavyweight payloads unless the real client does. Load tests should reproduce production message shapes, not stress unrelated code paths.

  • If your system uses ping/pong, heartbeat every client at the same interval you expect in production, not a synthetic “stress” interval.

  • Record both open time and message latency. A socket that opens quickly but processes messages slowly is still a user-visible failure.


Measure the signals that matter for lip-sync avatars


For streaming avatars, I would track four metrics first:


1. Connection establishment latency. This includes DNS, TLS, auth, and the server-side session setup that happens before the socket is useful.


2. Message round-trip time. Measure the time from client send to server acknowledgement for the control messages that gate speech or animation state.


3. Disconnect and reconnect success rate. Real users refresh pages, lose Wi-Fi, or navigate away. Your signaling tier should cleanly recover, not leak sessions or reject resumptions under pressure.


4. Stability over time. Memory growth, file descriptor exhaustion, or session accumulation often only appear after several minutes of sustained concurrency.


One subtle point: if your avatar pipeline includes an audio or video backend in addition to signaling, isolate the test. First validate the WebSocket tier by itself. Then combine it with media generation. Otherwise, you will not know whether the latency came from signaling, model inference, encoding, or transport.


Common mistakes that make websocket tests misleading


The biggest mistake is treating the test client like a passive probe. In a real avatar session, the client and server both participate in state progression. If your k6 script only opens a socket and sits idle, you are testing idle socket capacity, not session capacity.


Other failure modes to avoid:


  • Single message path only. Exercise all important states: join, update, speak, stop, close, reconnect.

  • Uniform timing. Add jitter to the test traffic if real users do not all act at exact intervals.

  • No cleanup. Make sure sockets close cleanly and sessions are released, or your test will fail for the wrong reason.

  • Ignoring auth cost. Token validation and session bootstrap are often a large fraction of connection time.

  • Overlooking server limits. Rate limits and per-session caps may be intentional. Test that they fail predictably.


Also, keep an eye on the distinction between concurrency and throughput. A signaling service can often handle many short-lived sockets or fewer long-lived interactive sessions, but not both at once at the same ceiling. Avatar workloads are usually closer to long-lived interactive sessions, so optimize your test accordingly.


How Protoface fits into this


If you are building avatars on Protoface, the useful part here is that the platform exposes developer-facing surfaces for session creation and management, plus a LiveKit Agents plugin for dropping a synchronized avatar into a voice agent. For signaling tests, you generally want to exercise the same session lifecycle your production client or agent uses, not a toy endpoint.


For programmatic setup, the Python SDK is the cleanest place to pull real session identifiers and create test fixtures. For example, you can provision an avatar or session in code, then point your k6 test at the resulting realtime endpoint. Keep the exact fields aligned with the docs, but the shape is straightforward:


from protoface import ProtofaceClient

)
from protoface import ProtofaceClient

)
from protoface import ProtofaceClient

)


If you are integrating through LiveKit, the plugin-based path is also worth testing because it reflects how the control plane behaves when a voice agent is actually driving the avatar. The relevant examples live in the integration repo and the package on PyPI, and the Pipecat guide is useful if that is your agent stack. For implementation details, use the docs rather than guessing the message contract.


For direct API orchestration, a small curl call is handy for sanity checks before you spin up k6:


curl -s https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123","mode":"realtime"}'
curl -s https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123","mode":"realtime"}'
curl -s https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123","mode":"realtime"}'


The main value of using the platform’s own session APIs in your load test is consistency: you are exercising the same auth, lifecycle, and cleanup paths that your application will use in production.


Conclusion


Load testing websocket signaling for lip-sync avatars is mostly about testing stateful control flow under concurrency. If you model the real session lifecycle, use realistic message timing, and track connection time, round-trip latency, reconnect behavior, and cleanup, you will get signal you can act on. That gives you a much better read on whether the avatar experience will remain stable when real users show up.


If you are implementing this against Protoface, start with the docs at docs.protoface.com, wire up a small authenticated session in the SDK or your agent stack, and then run k6 against the exact signaling path your application will use in production. That is the quickest way to separate “the avatar looks fine on my laptop” from “this will survive real traffic.”

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.