On this pageOPENCLOSE
ICE, STUN, TURN: finding a path
How peers discover addresses, traverse NAT, run connectivity checks, and fall back to a relay when nothing else works.
Goal
- 01Explain why two browsers behind NAT cannot simply connect to each other's private addresses.
- 02Use host, server-reflexive, peer-reflexive, and relay candidates without confusing what each one proves.
- 03Read an ICE trace, identify the selected pair, and know when TURN is mandatory rather than optional.
The concept, plainly
lets many private devices share one public address. A peer can open an outbound connection, but another peer cannot magically dial `192.168.1.20` on a different network. solves this by gathering several candidate addresses, exchanging them through signaling, and testing pairs until one path works. It is a connectivity search, not a single protocol message.
Candidate types tell you how an address was discovered. A **host** candidate is a local interface address. A **server-reflexive** (srflx) candidate is the public mapping a server observed. A **peer-reflexive** (prflx) candidate is a mapping discovered during connectivity checks that was not signaled. A **relay** candidate is a TURN allocation. More candidates mean more chances, not a guaranteed path.
is deliberately small: it reports the source address a server sees and helps keep NAT bindings open through connectivity checks. It does not relay media. If both peers are behind symmetric NAT or UDP is blocked, the direct pairs will fail. allocates a relay address, and both peers send media through that server. It works almost anywhere, costs bandwidth, and adds latency.
The selected pair is the one ICE nominates and the transport actually uses. Look at `getStats()` candidate-pair `nominated` and `selected`, or the transport's `selectedCandidatePairId`. Seeing a relay candidate in the list does not mean the call uses TURN. A direct host or srflx pair may win the checks first. Conversely, a call can work in the lab on one network and fail in a restrictive one without TURN configured.
Trickle ICE sends candidates as they are discovered, so media can start sooner. Non-trickle gathering waits for the complete candidate set and puts it in SDP. Both are valid. If candidates arrive before the remote description, queue them. When gathering finishes, an empty candidate event means complete, not failure. An `icecandidateerror` event names a STUN or TURN server problem.
When a network changes, call `restartIce()`, create a new offer, and let ICE find a fresh path. TURN credentials should be short-lived and issued by your own server; never ship a permanent TURN secret to the browser. Design for failure: show 'checking,' 'connected,' and 'failed' honestly, collect TURN usage metrics, and do not claim 'peer-to-peer' if every call is relayed.
See it move
Interactive diagram
How ICE finds a path across two NATs
Gather local host candidates
The browser lists private interface addresses. They can connect peers on the same LAN, but are usually unroutable across the internet.
Gather candidates, exchange them through signaling, test pairs, and nominate one. STUN discovers; TURN relays.
Behind the scenes
NAT mapping is not one behavior
Full-cone, restricted-cone, port-restricted, and symmetric NATs map and filter differently. A STUN-discovered port may work for one remote endpoint but not another. That is why a call can connect to one friend and fail to another from the same laptop. ICE tests the actual peer pair; STUN alone only predicts reachability from the server's viewpoint.
Candidate priority is a suggestion
ICE orders pairs by candidate type and priority, usually preferring host over srflx over relay. But the selected pair is the one that succeeds and gets nominated, not necessarily the first in the list. Mobile networks, VPNs, and firewalls can reorder the practical outcome. Always read the selected pair from stats rather than inferring from candidate order.
TURN is a production requirement
A lab without TURN can feel like WebRTC is broken when only one network is restrictive. Real products plan TURN capacity, credentials, and cost from day one. Relay usage can be a significant share of calls in enterprise and mobile networks. Metric it, geo-distribute relays, and issue credentials short-lived so a leaked config expires.
Code walkthrough
Use coturn's shared-secret REST scheme. The browser gets a username and HMAC credential that expire; the permanent secret stays on the server.
import { createHmac } from "node:crypto";export async function GET() {const secret = process.env.TURN_STATIC_SECRET;const turnUrl = process.env.TURN_URL;if (!secret || !turnUrl) {return Response.json({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });}const ttl = 60 * 60;const expiry = Math.floor(Date.now() / 1000) + ttl;const username = `${expiry}:learn0`;const credential = createHmac("sha1", secret).update(username).digest("base64");return Response.json({iceServers: [{ urls: "stun:stun.l.google.com:19302" },{ urls: turnUrl, username, credential },],ttl,});}
Do and don't
Do
Run TURN from the start
A direct path is an optimization, not a guarantee. Test with a restrictive network or forced relay before you assume connectivity is solved.
Expose the selected pair
Show whether a call is direct or relayed in diagnostics. This shortens support conversations and reveals TURN cost patterns.
Queue candidates and handle errors
Candidates can race descriptions, and individual STUN/TURN servers can fail. Treat both as normal protocol states.
Restart ICE on network change
Switching Wi-Fi, cellular, or VPN changes the path. Restart ICE with a new offer rather than silently freezing.
Don't
Don't call STUN a relay
STUN discovers an address; it does not carry media. If both peers need a relay, only TURN helps.
Don't ship static TURN secrets
A browser is not a secret store. Use expiring credentials minted by your server and rotate the shared secret.
Don't assume connected means direct
A connected call can be relayed. Read the selected pair before making latency or privacy claims.
Don't rely on one STUN URL
STUN outages and blocked servers happen. Configure redundancy and let candidate gathering continue even if one server fails.
Practice
Hands-on lab
Watch a path get chosen
The 1:1 lab lists every candidate, marks dropped ones, and reads the selected pair from live getStats. Use the filters to make direct paths fail.
- 1Connect two tabs normally and locate the selected pair's candidate types.
- 2Set the filter to TURN-only with no TURN configured, reset, and watch the connection fail in checking.
- 3Restore normal, reconnect, and confirm media returns.
- 4Read the ICE event log and explain why a candidate can be present but not selected.
Solo? Loopback still gathers host candidates and runs the same ICE state machine. Filters such as TURN-only will make the local connection fail too.
Quick quiz
Knowledge check
0 of 4 answered
- 01
What does a server-reflexive candidate prove?
- 02
When is TURN required?
- 03
Which stats field identifies the nominated pair currently used by the transport?
- 04
What should happen when a remote candidate arrives before setRemoteDescription?
Key takeaways
- ICE searches candidate pairs; STUN discovers a public view; TURN relays when direct paths fail.01
- Host, srflx, prflx, and relay are different kinds of possibility, not different qualities of the same address.02
- The selected pair in getStats is the truth about direct versus relayed media.03
- Production WebRTC plans for TURN credentials, relay cost, candidate ordering, and network changes.04
Common errors and fixes
Terms in this lesson
Done with this lesson?
Mark it complete, then keep the thread.