WebRTC Cheatsheet
ICE, STUN, and TURN
Use this WebRTC reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
ICE Overview
ICE (Interactive Connectivity Establishment) is the algorithm that finds a working network path between two peers. It gathers candidates, prioritizes them, and tries them in order.
Candidate Types (priority order)
| Type | Description | Works across NAT? |
|---|---|---|
| host | Local IP/port on the machine | Same LAN only |
| srflx (server-reflexive) | Public IP/port via STUN | Most NAT types |
| prflx (peer-reflexive) | Discovered during connectivity checks | Yes |
| relay | Traffic routed through TURN server | Always |
STUN — Session Traversal Utilities for NAT
STUN tells a peer its public IP and port (server-reflexive candidate) without relaying traffic.
const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, // Google public STUN { urls: 'stun:stun1.l.google.com:19302' }, { urls: 'stun:stun.cloudflare.com:3478' }, // Cloudflare public STUN { urls: 'stun:stun.stunprotocol.org:3478' }, ], });
STUN works for ~80 % of connections (full-cone, address-restricted, port-restricted NAT). Symmetric NAT requires TURN.
TURN — Traversal Using Relays around NAT
TURN relays all media through a server — always works but adds latency and bandwidth cost.
const pc = new RTCPeerConnection({ iceServers: [ { urls: [ 'turn:turn.example.com:3478', // UDP 'turn:turn.example.com:3478?transport=tcp', // TCP fallback 'turns:turn.example.com:5349', // TLS (firewalls blocking UDP) ], username: 'user123', credential: 'secret456', }, ], });
TURN URL Schemes
| Scheme | Transport | Port (default) |
|---|---|---|
stun: | UDP | 3478 |
stuns: | UDP + DTLS | 5349 |
turn: | UDP or TCP | 3478 |
turn:…?transport=tcp | TCP | 3478 |
turns: | TCP + TLS | 5349 |
Short-Lived TURN Credentials (Server-Side)
Never embed long-lived TURN credentials in client JS. Generate time-limited credentials:
// Server (Node.js) — HMAC-based credential generation import crypto from 'crypto'; function generateTurnCredentials(username, secret, ttlSeconds = 3600) { const expires = Math.floor(Date.now() / 1000) + ttlSeconds; const tempUser = `${expires}:${username}`; const credential = crypto .createHmac('sha1', secret) .update(tempUser) .digest('base64'); return { username: tempUser, credential, expires }; } // Client fetches from your API const res = await fetch('/api/turn-credentials'); const { username, credential } = await res.json(); const pc = new RTCPeerConnection({ iceServers: [{ urls: 'turn:turn.example.com', username, credential }], });
ICE Transport Policy
// Default: try all candidate types const pc = new RTCPeerConnection({ iceTransportPolicy: 'all' }); // Force relay through TURN (ensures media always goes through your server) const pc = new RTCPeerConnection({ iceTransportPolicy: 'relay' });
Use
'relay'to enforce routing through your infrastructure (compliance, recording, content moderation). Never use it as a debugging shortcut — it hides connection issues.
ICE Candidate Pool
Pre-gather candidates before setLocalDescription to reduce setup latency:
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], iceCandidatePoolSize: 10, // gather 10 candidates in advance });
Monitoring ICE Progress
pc.onicecandidate = ({ candidate }) => {
if (!candidate) {
console.log('ICE gathering complete');
return;
}
console.log('Candidate type:', candidate.type); // 'host'|'srflx'|'relay'
console.log('Candidate address:', candidate.address);
console.log('Candidate port:', candidate.port);
console.log('Protocol:', candidate.protocol); // 'udp'|'tcp'
console.log('Priority:', candidate.priority);
console.log('Full candidate:', candidate.candidate); // SDP line
};
pc.onicecandidateerror = (event) => {
console.error('ICE error', event.errorCode, event.errorText, event.url);
// Common errors:
// 701 — STUN server not reachable
// 702 — STUN server returned error
// 703 — STUN server: bad response
// 704 — TURN server returned error
};
pc.oniceconnectionstatechange = () => {
console.log('ICE state:', pc.iceConnectionState);
};
pc.onicegatheringstatechange = () => {
console.log('Gathering state:', pc.iceGatheringState);
};ICE Connection States
iceConnectionState | Meaning |
|---|---|
"new" | Not started |
"checking" | Sending STUN checks |
"connected" | At least one candidate pair works |
"completed" | All checks done; best pair selected |
"failed" | No working pair found |
"disconnected" | Connection dropped (may recover) |
"closed" | Connection closed |
ICE Gathering States
iceGatheringState | Meaning |
|---|---|
"new" | Not started |
"gathering" | Actively collecting candidates |
"complete" | All candidates gathered |
Reading the Chosen Candidate Pair
// After connection is established const statsReport = await pc.getStats(); statsReport.forEach(report => { if (report.type === 'candidate-pair' && report.nominated && report.state === 'succeeded') { console.log('RTT:', report.currentRoundTripTime * 1000, 'ms'); console.log('Bytes sent:', report.bytesSent); console.log('Bytes received:', report.bytesReceived); // Look up the local and remote candidate details const local = statsReport.get(report.localCandidateId); const remote = statsReport.get(report.remoteCandidateId); console.log('Local type:', local?.candidateType); // 'host'|'srflx'|'relay' console.log('Remote type:', remote?.candidateType); console.log('Protocol:', local?.protocol); // 'udp'|'tcp' } });
ICE Restart
Use when the connection drops (network change, sleep/wake):
pc.oniceconnectionstatechange = async () => { if (pc.iceConnectionState === 'failed') { // Attempt ICE restart const offer = await pc.createOffer({ iceRestart: true }); await pc.setLocalDescription(offer); signaling.send({ type: 'offer', sdp: pc.localDescription }); } };
Self-Hosted TURN (coturn)
# Install coturn apt install coturn # /etc/turnserver.conf — minimal config listening-port=3478 tls-listening-port=5349 fingerprint lt-cred-mech use-auth-secret static-auth-secret=YOUR_SECRET_HERE realm=turn.example.com cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem log-file=/var/log/turnserver.log
Popular Public TURN/STUN Providers
| Provider | Free Tier | Notes |
|---|---|---|
| Twilio Network Traversal Service | Pay-as-you-go | Most reliable; geo-distributed |
| Cloudflare Calls | Generous free tier | Integrated with CF Workers |
| Metered.ca | 50 GB/mo free | Open-source friendly |
| coturn (self-hosted) | Unlimited | You manage infra |
Google STUN (stun.l.google.com) | Free | STUN only, no TURN |
Common ICE Gotchas
- Symmetric NAT (often corporate) blocks STUN-only connections — always configure TURN as a fallback.
iceConnectionState === 'disconnected'is transient — don't close the connection immediately; wait for'failed'before restarting.- UDP blocked on port 3478 — configure
turns:(TLS on 443) as a fallback; port 443 is almost never blocked. - TURN with
iceTransportPolicy: 'all'— TURN is only used when no direct path works; if you need guaranteed relay, use'relay'. - Candidates with
addresshidden — browsers may anonymize host candidates via mDNS (xxxxxxxx.local); this is normal and resolves during ICE checks. - Multiple TURN servers — only use 2–3; too many cause excessive candidates and slow gathering.