Debugging WebRTC Disconnects in a Go Realtime Healthcare Avatar

Debugging WebRTC avatar disconnects in Go: signaling, ICE, stats, timeouts, and LiveKit lifecycle tracing.
Introduction
When a realtime avatar “disconnects,” the failure is usually not in one place. A WebRTC session can break because the browser lost ICE connectivity, the media server stopped forwarding RTP, the backend failed to keep the session alive, or the application-level signaling between your voice agent and avatar timed out. In a Go backend, those failures tend to look like ordinary network churn until you trace the entire path from signaling to media to agent state.
This post walks through a practical debugging approach for a Go service that drives a realtime healthcare avatar: how to classify the disconnect, what to instrument, how to reproduce the problem, and how to separate WebRTC transport issues from application bugs. By the end, you should have a repeatable checklist for diagnosing why an avatar froze, went silent, or dropped out mid-conversation.
Start by classifying the failure mode
“Disconnected” is too broad to be actionable. In a realtime avatar pipeline, there are usually three layers to inspect:
Signaling: SDP exchange, session creation, token/auth, offer/answer delivery, and any REST calls used to create the session.
Transport: ICE candidate gathering, NAT traversal, DTLS handshake, RTP/RTCP media flow, and TURN fallback if direct connectivity fails.
Application state: the agent keeps speaking but the avatar stops rendering, the avatar session expires, or the backend closes an idle websocket/HTTP stream.
In practice, the symptom tells you where to look. For example:
No video ever appears: signaling or auth is usually broken.
Video appears, then freezes after 10–60 seconds: ICE or network instability is a common cause.
Avatar keeps moving but audio stops: media track or downstream TTS pipeline issue.
Session dies at a fixed time: expiration, rate limit, or application timeout.
The most useful thing you can do early is assign each session a correlation ID and log it everywhere: session creation, WebRTC negotiation, voice agent start, avatar attach, reconnect, and teardown. If you only log “disconnect,” you’ll spend the rest of the day guessing.
Instrument the WebRTC path, not just the app logs
When debugging WebRTC, you want visibility into both signaling and media state. A backend log showing “session created” is not enough. You need the browser or client-side connection state transitions, plus periodic snapshots of the peer connection stats.
Watch the connection state machine
At a minimum, log these transitions from the client side:
iceConnectionStateconnectionStatesignalingStatetrack events for
ontrack,onicecandidate, andonnegotiationneeded
A healthy session typically goes through new → checking → connected. If it gets stuck in checking, suspect NAT traversal, missing TURN, or blocked UDP. If it flips to disconnected and then failed, the network path likely broke or the peer stopped responding to consent checks.
For a quick browser-side repro, open DevTools and look at the WebRTC internals in Chrome, or use chrome://webrtc-internals to inspect candidate pairs, bitrate, packet loss, and RTCP events. You’re looking for the moment media stops flowing, not just the moment your UI noticed it.
Use stats to distinguish packet loss from a hard teardown
The trap with realtime avatars is mistaking a media degradation event for a disconnect. If the avatar looks frozen, the WebRTC connection may still be alive, but the video bitrate has collapsed or RTCP reports sustained loss. Pull getStats() periodically and compare the following:
bytesSent/bytesReceivedover timepacketsLostand jitterselected candidate pair and protocol (UDP vs TURN/TCP)
RTT spikes immediately before failure
In Go, your backend may not own the peer connection directly, but you can still correlate upstream events with media behavior. A practical pattern is to record the server-side timestamp when you send session credentials, then compare it with the client-side timestamp when ICE connects. If the delta is consistently large or variable, your signaling path is slow or unstable.
Reproduce under controlled network conditions
Production disconnects are often environment-specific. To get a meaningful signal, reproduce with deliberate impairment:
Throttle bandwidth and add latency.
Block UDP to force TURN/TCP or fail fast.
Switch networks mid-session to test ICE restart behavior.
Leave the tab idle long enough to hit any heartbeat or session expiration logic.
On Linux, tc netem is enough to make a flaky network deterministic. On a corporate laptop behind aggressive firewall rules, TURN fallback is often the difference between “works locally” and “disconnects in production.” If your avatar depends on direct peer-to-peer media, make sure you know what happens when UDP is unavailable.
Also check whether your Go service is closing something prematurely: an HTTP request context, a websocket, or a goroutine tied to the parent request lifecycle. In realtime systems, it is easy to accidentally couple a long-lived media session to a short-lived web request.
Common Go-side bugs that look like WebRTC issues
Several backend problems masquerade as transport failures:
Context cancellation: the request scope ends, and cleanup code tears down the session.
Timeout mismatch: the server expects a keepalive every N seconds, but the client sends less frequently.
Race on session state: one goroutine marks the session closed while another is still negotiating.
Stale credentials: tokens or session IDs expire before the client finishes connecting.
Over-aggressive retry: reconnect logic creates a new session while the old one is still active, causing duplicate state and confusing media teardown.
A simple debugging rule: if the disconnect happens at the same wall-clock interval across many clients, inspect your own timers before blaming the network.
Example: trace a session from Go with explicit logging
If your backend creates avatar sessions via REST, log the request/response pair with a correlation ID and session identifier. Keep the payload minimal and avoid logging secrets.
This is not about the exact endpoint shape; it’s about capturing timing and ownership. Once you know whether the failure occurs before the session exists, during negotiation, or after media starts, the search space shrinks dramatically.
Example: watch LiveKit agent lifecycle events
If you are embedding a talking face into a voice agent, the avatar lifecycle should be tied to the agent lifecycle, not the request lifecycle. The plugin layer should initialize with the agent, attach the video track, and tear down only when the agent is actually finished.
The key debugging question here is: did the avatar detach because the agent ended, or did the agent end because the avatar session broke? Log both directions. Avoid silent cleanup.
How Protoface fits into the debugging workflow
One useful part of the stack is the developer-facing control plane: create and inspect sessions through the REST API, then compare what the backend thinks happened with what the browser or agent actually observed. For quick checks, the public docs at docs.protoface.com are the right place to confirm the session fields, lifecycle behavior, and supported integration patterns.
In practice, this means you can create a session, attach it to a voice agent or client, and then compare server-side timestamps with client-side WebRTC state transitions. That separation is helpful: if the API says the session is active but the client never reaches connected, the bug is in signaling or network setup. If the client connects and then drops, focus on transport stability, heartbeat logic, or your own teardown path.
If you are using the LiveKit plugin, the examples in the relevant repository are a better debugging reference than generic WebRTC samples because they show where the avatar lifecycle is supposed to hook into the agent lifecycle. For backend-owned session flows, the REST API is the cleanest place to confirm whether the issue starts before media ever moves.
Conclusion
Most WebRTC disconnects in realtime avatar systems are diagnosable once you separate signaling, transport, and application state. Log the connection state machine, collect stats, reproduce under bad network conditions, and verify that your Go service is not closing sessions too early or too aggressively. The fastest path to a fix is usually a precise classification of where the failure begins, not a bigger retry loop.
If you’re integrating a realtime avatar into a Go-backed healthcare workflow, start by instrumenting session creation and client connection state, then compare that against the avatar lifecycle in your agent. For implementation details and supported options, check docs.protoface.com and the relevant integration examples in the Protoface ecosystem.
