Header Logo

Debugging WebRTC and WebSocket Disconnects in a Realtime Express Avatar Widget

Debugging WebRTC and WebSocket Disconnects in a Realtime Express Avatar Widget

Debug WebRTC/WebSocket disconnects in a realtime Express avatar widget with session logging, ICE stats, and reconnection fixes.

Introduction


When a realtime avatar widget “just drops,” the failure mode is often ambiguous: the browser says the WebSocket closed, the peer connection is in failed, the audio track is silent, or the video freezes after a few seconds. Those symptoms can come from the transport layer, the signaling layer, the media pipeline, or the application code that glues them together.


This post walks through a practical debugging workflow for a realtime Express-style avatar widget built on WebRTC for media and WebSocket for signaling. By the end, you should be able to separate connection establishment problems from media negotiation issues, pinpoint where disconnects originate, and add enough logging to make the next incident obvious instead of mysterious.


Understand the two connections you are actually debugging


Most realtime avatar widgets use at least two independent channels:


  • WebSocket for control plane traffic: auth, session setup, event signaling, state updates, and sometimes incremental transcription or avatar commands.

  • WebRTC for media plane traffic: audio, video, and sometimes data channels.


This matters because “disconnect” is not a single event. A WebSocket can close while the media connection survives briefly. A WebRTC peer connection can fail even though the app-level WebSocket remains open. If you treat them as one thing, you’ll chase the wrong layer.


Start by asking four questions:


  1. Did the WebSocket close first, or did the peer connection fail first?

  2. Was the failure during signaling, ICE gathering, ICE connectivity checks, DTLS/SRTP setup, or media flow?

  3. Did the server actively close the session, or did the browser lose network connectivity and tear everything down?

  4. Is the issue reproducible on one network, one browser, or one embed configuration?


If you don’t already log these separately, do it now. At minimum, track:


  • WebSocket open/close/error events and close codes

  • RTCPeerConnection.connectionState

  • RTCPeerConnection.iceConnectionState

  • RTCPeerConnection.signalingState

  • time to first remote track and time to first audio packet


Instrument the browser so you can name the failure mode


The fastest way to debug a realtime widget is to make the browser tell you exactly what it thinks happened. A minimal event logger usually pays for itself immediately.


const ws = new WebSocket(signalingUrl);
const ws = new WebSocket(signalingUrl);
const ws = new WebSocket(signalingUrl);


Interpretation is straightforward:


  • iceConnectionState = checking forever usually means NAT traversal, TURN, firewall, or candidate exchange problems.

  • connectionState = failed after a brief success often means the network changed or the selected candidate pair stopped working.

  • WebSocket close code 1006 usually means the browser never got a clean close frame; think network interruption, proxy reset, or server crash.

  • WebSocket close code 1008 often indicates policy or auth rejection.

  • WebSocket close code 1011 points toward an internal server problem.


Use the browser’s WebRTC internals while reproducing the issue. In Chrome, chrome://webrtc-internals will show candidate pair selection, RTP stats, bitrate, packet loss, jitter, and whether the connection ever moved from checking to connected. If the remote track arrives but audio never plays, you likely have autoplay or media track issues rather than transport failure.


Debug the signaling path separately from the media path


It’s common to assume a bad avatar stream means WebRTC is broken, when the real issue is that signaling never completed. WebRTC needs offer/answer exchange and ICE candidate exchange before media can flow. If any of those messages are lost, malformed, or sent out of order, the peer connection can appear “half alive.”


For signaling debug, log the exact order of these milestones:


  1. WebSocket connected

  2. Session authenticated

  3. Offer created or received

  4. Answer applied

  5. ICE candidates gathered and exchanged

  6. ICE connected

  7. Remote track received


A useful pattern is to attach a correlation ID to every session and include it in both client and server logs. If your widget is embedded in customer sites, include the embed origin and a short session identifier. When a disconnect happens, you want to answer “which session, on which origin, under which network conditions?” without searching through unrelated traffic.


Pay attention to ordering bugs. A few common ones:


  • Applying remote ICE candidates before the remote description is set.

  • Sending an offer before the WebSocket auth handshake is complete.

  • Reusing a stale WebSocket after the page restored from back/forward cache.

  • Creating multiple peer connections when a React component remounts.


If your widget runs inside an iframe, also verify that the parent page is not tearing down and recreating the iframe on every render. That can look like a transport bug when it’s actually a lifecycle bug.


Read the network and browser failure signals correctly


A lot of disconnect debugging comes down to distinguishing infrastructure failures from browser policy. A few examples:


  • Proxy or load balancer resets: WebSocket dies with 1006, often after a predictable idle timeout. Check upstream idle settings and heartbeat intervals.

  • ICE/TURN reachability problems: WebRTC stalls in checking or fails after candidate exchange. Test on a different network, then confirm TURN availability and credentials.

  • Autoplay restrictions: Remote track exists, but video/audio does not start until user gesture. This is not a network issue.

  • Cross-origin embedding mistakes: The iframe is blocked, sandboxed too aggressively, or the parent origin is not allowed.

  • Server-side session expiry: The widget disconnects on a schedule. Check token/session lifetime, not packet loss.


For WebSockets, heartbeat behavior matters. If the server expects pings and the client is behind an aggressive NAT or proxy, the socket may look healthy until the intermediary silently drops it. Conversely, if the client treats brief network loss as fatal, mobile users will see frequent reconnects. Decide explicitly whether your app should reconnect, resume, or end the session when the socket closes.


For WebRTC, remember that media can degrade before it disconnects. Look at packet loss, RTT, and jitter before assuming the peer connection is dead. A session with high loss may still be technically connected while sounding broken. That distinction is useful when deciding whether to retry, downgrade quality, or alert the user.


Make reconnection deliberate, not accidental


Realtime widgets need a reconnection policy. The worst behavior is a hidden infinite retry loop that creates new sessions and leaks resources. A better approach is:


  1. Detect whether the failure was transient or terminal.

  2. Reuse the same logical session only if the server supports it.

  3. Bound retries with backoff and jitter.

  4. Surface a clear UI state when recovery is not possible.


In practice, treat these as terminal unless you have explicit resume support:


  • Auth failures

  • Expired session tokens

  • Policy violations

  • Repeated ICE failures across retries


Transient candidates for retry:


  • Brief network blips

  • Proxy resets

  • Page visibility transitions, if your app intentionally pauses media


On the client, a reconnect should create fresh signaling state and, usually, a fresh peer connection. Reusing a failed RTCPeerConnection is error-prone because internal state can remain inconsistent after negotiation errors or ICE failure.


Where Protoface fits


This is the part where a developer platform can reduce the amount of glue code you have to debug. Protoface gives you a controlled avatar session layer plus documented integration points, so you can focus on the transport behavior instead of building your own media/session plumbing from scratch. If you are embedding an avatar in a voice agent, the LiveKit plugin path is a good example because it keeps the avatar synchronized with the agent while still letting you inspect the underlying realtime events. If you prefer to create sessions programmatically, the REST API and Python SDK let you instrument session creation and teardown on your side while keeping auth and lifecycle explicit.


For example, creating or inspecting a session via the API is the kind of operation you can log alongside your own connection IDs:


curl -X POST https://api.protoface.com/v1/sessions \
curl -X POST https://api.protoface.com/v1/sessions \
curl -X POST https://api.protoface.com/v1/sessions \


And if you are using the Python SDK, keep the session creation call close to your connection logging so you can correlate server-side state with the browser trace. Exact method names and fields are in the docs, but the important point is the same: create a single source of truth for session IDs, then propagate it into your frontend logs.


If your deployment is iframe-based, the same debugging advice still applies, but the browser lifecycle becomes even more important. Parent-origin allowlists, per-embed settings, and browser sandboxing can all look like transport failures if you don’t explicitly log embed origin, iframe load events, and message handshakes.


Conclusion


Most realtime disconnect bugs are not “WebRTC bugs” in the abstract. They are usually one of four things: signaling ordering, network reachability, browser/media policy, or lifecycle mistakes in the embedding app. The practical fix is to instrument each layer separately, correlate events with a session ID, and read WebSocket and WebRTC state transitions as distinct signals.


Once you can tell where the failure starts, the rest becomes routine: check auth, check signaling order, inspect ICE stats, verify proxy timeouts, and test on a second network. For integration details and surface-specific guidance, start with the docs at docs.protoface.com.

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.