How to Debug ICE Connection Failures in WebRTC AI Avatar Apps Behind NAT and Firewalls

Debug ICE failures in WebRTC avatar apps behind NAT/firewalls: signaling vs transport, candidates, TURN, and browser logs.
Introduction
ICE connection failures are the part of WebRTC that tend to fail “mysteriously” in production: the app works on your laptop, then silently breaks for users behind corporate firewalls, symmetric NATs, VPNs, or aggressive mobile carriers. If you’re building an AI avatar app, that failure is especially painful because the media path is the product: audio never reaches the agent, the avatar never starts, or the session stalls after signaling succeeds.
This post focuses on how to debug those failures systematically. By the end, you should be able to tell whether you have a signaling problem, an ICE candidate problem, a TURN allocation problem, or a network policy problem; how to inspect the right logs and browser state; and how to reduce the blast radius in production.
What ICE is actually doing, and why it fails behind NAT
ICE is the process WebRTC uses to find a viable network path between peers. The browser gathers candidates: host candidates from local interfaces, server-reflexive candidates from STUN, and relay candidates from TURN. It then runs connectivity checks over candidate pairs until one succeeds.
In a simple home network, this often “just works.” Behind NAT and firewalls, it may not. Common failure modes include:
No viable candidate pair: only host candidates are gathered, but those addresses are private and unreachable.
STUN works, direct path fails: NAT mapping exists but inbound packets are blocked or the NAT is symmetric.
TURN is reachable on paper, blocked in practice: UDP to the TURN server is blocked, or the firewall only allows 443/TCP and the client never falls back to TURN/TCP or TLS.
ICE completes too late: the agent or avatar times out before a relay path is established, especially on restrictive enterprise networks.
For avatar apps, one subtlety matters: signaling can succeed even when media cannot. You may receive session metadata, create the peer connection, and even exchange SDP, but still never establish a usable media transport. So the debugging target is not “did the API respond?” but “did ICE nominate a working pair and keep it alive?”
Start by separating signaling failures from transport failures
Before you look at STUN/TURN details, determine where the failure occurs:
Signaling failure: no offer/answer exchange, bad auth, mismatched SDP, or session creation failed.
ICE gathering failure: the browser cannot gather candidates, often because the STUN/TURN servers are unreachable or misconfigured.
Connectivity check failure: candidates exist, but no pair succeeds; this is the classic NAT/firewall problem.
Post-connect media failure: ICE is connected, but audio/video still doesn’t flow due to codec, track, or server-side issues.
The browser is your first source of truth. In Chrome DevTools, inspect the RTCPeerConnection state transitions:
Useful state patterns:
iceGatheringStatestays atneworgatheringforever: candidate gathering is blocked or never finishes.iceConnectionStategoeschecking→failed: the selected candidates could not establish connectivity.connectionStatebecomesconnectedbriefly thendisconnectedorfailed: path was established, then NAT binding or firewall policy changed.
On the server or agent side, check whether the session sees remote candidates at all. If you are using LiveKit-based infrastructure, the same pattern applies: signaling may look healthy while media transport is not.
Inspect candidates, not just states
State transitions tell you that something failed. Candidate inspection tells you why. Dump the local and remote candidates and look at types, IP family, and ports.
What you want to see in a restrictive environment is a relay candidate from TURN, often over TCP or TLS on port 443. If you only see host candidates, the browser never got a usable relay path. If you see relay candidates but they never win, connectivity checks are failing or a lower-layer firewall is dropping packets.
A few practical interpretations:
IPv6-only or IPv4-only environments: candidate family mismatches can cause failures that look random.
Symmetric NAT: server-reflexive candidates may exist but still be unusable for inbound connectivity.
Enterprise firewalls: UDP may be blocked entirely, so TURN-over-UDP works in testing and fails in customer networks.
Captive portals / proxies: initial connectivity checks may fail until the network is fully authenticated, or long-lived UDP flows are interrupted.
Use TURN intentionally, not as an afterthought
When users are behind NAT and firewall constraints you do not control, TURN is usually the difference between “works in the lab” and “works in production.” The point of TURN is not better latency; it is reachability. If peer-to-peer is impossible, the media relays through a server that both sides can reach.
For debugging, the key questions are:
Are TURN credentials being provisioned correctly and expiring when expected?
Is the client actually receiving relay candidates?
Can the client reach the TURN server over at least one transport?
Does the network allow the candidate pair that ICE ultimately selects?
From a browser console, you can verify whether a TURN relay is in play by inspecting the selected candidate pair after connection:
If the selected local candidate is relay, you are at least past the “direct path” problem. If the relay path still fails, focus on TURN reachability, credentials, and firewall transport rules. In practice, a TURN server on 443/TCP or 443/TLS is often the only path through stricter networks, even though it is not the most efficient route.
Debugging tactics that save time in production
When an app only fails for some users, collect enough detail to classify the failure without asking for a screen recording. The minimum useful bundle is:
Browser and OS version
Public network type: home, corporate, hotel, mobile carrier, VPN
ICE state transitions with timestamps
Local/remote candidate types and protocols
Whether a relay candidate was nominated
Any
onicecandidateerroroutput
Then reproduce with network constraints that approximate the user environment:
Disable Wi-Fi and test on mobile hotspot.
Test behind a corporate proxy or firewall if you have one available.
Force UDP blocking at the firewall or router if you control the environment.
Compare a working path and a failing path side by side, not just the “failed” case.
Two common mistakes:
Assuming STUN is enough: it is not enough for networks that block inbound traversal or use symmetric NAT.
Only checking “connected”: some apps connect briefly, then lose media as NAT bindings expire or the network changes. Watch for
disconnectedandfailedtransitions.
If you own the signaling and ICE server config, make your fallback strategy explicit. In practice that means preferring robust relay paths for restrictive networks rather than assuming the browser will always find a direct route. The right answer is usually “give ICE enough options,” not “pick one candidate and hope.”
Where Protoface fits in
If you are embedding a realtime avatar into a voice agent or web session, you still debug the same WebRTC transport layers; the avatar does not change ICE. What it does change is the surface area you have to reason about. With the LiveKit plugin, you drop the avatar into an existing agent stack, and the browser-side path still has to survive NAT and firewall behavior. That means the same candidate-state checks, relay verification, and network triage apply.
For example, if you are using the LiveKit Agents plugin, keep an eye on the underlying media path as you integrate the avatar layer. The plugin repo and quickstart examples are a good reference point when you want to confirm whether a failure is in your agent logic or in the transport beneath it: GitHub organization and docs.
Likewise, if you are creating sessions through the REST API or Python SDK, use them to inspect whether the session is being created correctly, but do not confuse successful session creation with a successful ICE negotiation. Session setup and media connectivity are related, not identical.
Conclusion
ICE failures are usually not “WebRTC being flaky”; they are your network assumptions colliding with real NAT and firewall policy. Debug them by separating signaling from transport, inspecting candidate types and selected pairs, and verifying whether TURN relay paths are actually available and reachable. If direct connectivity works in one environment and fails in another, the problem is almost always network path selection, not the avatar logic itself.
For implementation details, integration examples, and the exact session and embed fields, start with docs.protoface.com. If you need a working reference for agent integration, the relevant plugin and quickstart repos are the fastest way to compare your setup against a known-good path.
