#WebRTC #debugging #ICE #TURN #networking

Debugging 'ICE Failed' in Production: A Practical Field Guide

ExpressTURN ExpressTURN · · 5 min read

"ICE Failed" is the most uninformative error message in the WebRTC stack. It tells you the connection didn't establish but says nothing about why. Six completely different root causes all surface as the same string. The right debugging approach is to systematically narrow down which one is biting you, not to randomly tweak config and hope.

This post walks through that process: the tools, the order to use them in, and the fix for each of the common root causes.

What ICE Actually Does

Quick refresher so the debugging makes sense. ICE (Interactive Connectivity Establishment) gathers a list of candidate addresses from each peer, exchanges them through your signaling channel, and then tests every pair until one works. Three kinds of candidates:

  • Host candidates: the peer's local IP addresses (LAN, WAN if directly connected).
  • Server-reflexive candidates: the peer's public address as observed by a STUN server.
  • Relayed candidates: a port allocated on a TURN server that forwards traffic to the peer.

"ICE Failed" means none of the candidate pairs successfully established two-way packet flow. Your job is to figure out at which step it broke.

Step 1: Open chrome://webrtc-internals

This is the first thing to do, every time. Open chrome://webrtc-internals in another tab, then start a call that fails. The page will show one section per active RTCPeerConnection.

Look for the connection corresponding to the failing call. Inside it you'll see:

  • iceConnectionState: should progress through checking to connected. If it stops at checking and then goes to failed, that's your symptom.
  • iceGatheringState: should reach complete. If it never does, your STUN/TURN config is wrong.
  • A list of candidate-pair stats. Each pair has a state. Look for any with state succeeded. If none, you're in real ICE failure territory.

Step 2: Check What Candidates Got Gathered

Scroll to the local-candidate entries in webrtc-internals. You should see multiple candidates per peer. Specifically look for:

  • type: host: means local-network gathering worked. Always present unless something is severely broken.
  • type: srflx: means STUN reachability worked. If missing, your STUN server is unreachable or the network blocks STUN's outbound port.
  • type: relay: means TURN allocation succeeded. If missing, your TURN credentials are bad, the TURN server is unreachable, or you didn't list TURN in iceServers.

If you see no relay candidates and your config has TURN listed, that's your problem. Skip ahead to the credentials check below.

Step 3: Force-Relay to Isolate TURN Issues

If candidates look fine but the call still fails, the next test is to force every packet through TURN. This tells you whether the relay path itself works:

const pc = new RTCPeerConnection({
  iceServers: [/* your normal config */],
  iceTransportPolicy: 'relay'  // force everything through TURN
});

If the call works with force-relay on, your TURN setup is fine and the failure is happening on the direct/STUN paths (probably symmetric NAT or UDP blocking on one end). The fix: make sure your TURN config has TCP and TLS-443 fallbacks, then let ICE pick.

If the call still fails with force-relay on, your TURN itself is broken. Continue to step 4.

Step 4: Validate TURN Credentials

The most common TURN failure is bad credentials. Two patterns to check:

Static long-term credentials: are the username and password literally what your provider issued? Copy-paste from the dashboard, no trailing whitespace, no string-escape weirdness.

Shared-secret credentials: if you're generating per-session credentials with HMAC, the most common bug is clock skew or wrong expiry format. The username must be ${expiry_unix_timestamp}:${user_id} and the credential must be the base64-encoded HMAC-SHA1 of that username using your shared secret. Generate one server-side, paste it into the browser console, and try a force-relay call. If that works but your normal call flow doesn't, your generation logic is broken.

// Quick TURN credential test
const credential = 'YOUR_FRESHLY_GENERATED_CREDENTIAL';
const username = 'YOUR_FRESHLY_GENERATED_USERNAME';
const pc = new RTCPeerConnection({
  iceServers: [{
    urls: 'turns:relay1.expressturn.com:443?transport=tcp',
    username, credential
  }],
  iceTransportPolicy: 'relay'
});
pc.oniceconnectionstatechange = () => console.log('ICE:', pc.iceConnectionState);
const dc = pc.createDataChannel('test');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// If you see iceConnectionState progress to 'connected', creds are good

Step 5: Check the Selected Candidate Pair

If a connection eventually establishes but feels flaky, look at which candidate pair won. Search webrtc-internals for selected-candidate-pair. The pair will tell you the local and remote candidate types.

  • host-host: direct LAN. Free and lowest-latency.
  • srflx-srflx: STUN-discovered direct. Free and good.
  • relay-anything: the relay is carrying media. Higher latency, costs you bandwidth.

If you expected a direct path but ended up on relay-relay, one of the peers is behind a restrictive NAT. That's not necessarily a bug, but it means you're paying for relay traffic on calls that you wish were direct.

Common Root Causes, Ranked

  1. No TURN configured. 60% of "ICE Failed" tickets in production. The fix is adding TURN.
  2. TURN configured but credentials wrong. 20% of cases. Force-relay reveals it immediately.
  3. TURN URL has UDP only, no TCP/TLS fallback. 10% of cases. Restrictive networks block 3478 entirely; you need 443.
  4. Both peers behind symmetric NAT, no TURN. Subset of #1.
  5. Stale shared-secret credential. The credential expired between generation and use. Generate with a longer TTL or generate at call-start, not at page-load.
  6. Server-side bug returning wrong ICE config to client. Always log the exact iceServers array your server hands the client, and verify it in webrtc-internals.

The Fix Pattern

Most "ICE Failed" cases collapse to one of two fixes:

  • Add TURN with UDP, TCP, and TLS-443 transports listed.
  • Verify TURN credentials work via force-relay testing.

Once both are in place, your ICE Failed rate drops below 1%. The remaining failures are usually genuine network outages or browser bugs that no config change can fix.

Need TURN credentials to test against? Sign up at ExpressTURN, free 1 TB/month, no credit card.

Need TURN servers for your WebRTC project?

Sign Up, Free 1 TB/month
← Back to Blog