WebRTC lets browsers establish direct peer-to-peer media channels without a central media server.
ICE (Interactive Connectivity Establishment) gathers candidate network paths and picks the fastest working one.
STUN (Session Traversal Utilities for NAT) discovers your public IP and port behind NAT.
TURN (Traversal Using Relays around NAT) relays media when direct P2P fails — adds ~150ms latency.
SDP (Session Description Protocol) declares media capabilities (codecs, encryption, bandwidth).
Production failure pattern: ICE timeout with "ICE failed, see about:webrtc" — usually a blocked STUN port or missing TURN server.
✦ Definition~90s read
What is WebRTC?
WebRTC is a browser-to-browser protocol for real-time audio, video, and data transfer—no plugins, no intermediate servers for media. It solves the historically hard problem of establishing a direct peer-to-peer connection across the hostile terrain of the public internet, where NATs, firewalls, and symmetric routing conspire to block unsolicited traffic.
★
Imagine you and a friend want to pass notes in class without the teacher (a server) reading every single one.
The core insight: WebRTC doesn't try to punch through everything; instead, it uses a layered negotiation system (ICE) that tries direct paths first, falls back to STUN for address discovery, and finally to TURN relays when NATs are too restrictive. This is not a generic streaming protocol—you'd use it for latency-sensitive apps like video calls (Google Meet, Discord), file sharing, or gaming, not for one-to-many broadcast or stored video playback.
The gotchas live in the details: ICE candidate gathering can take seconds on flaky networks, STUN servers can be blocked by enterprise firewalls, TURN relays cost bandwidth (AWS TURN runs ~$0.10/GB), and SDP offers are opaque strings that break silently if you mangle a line. Most production failures aren't in the media path—they're in the signaling handshake that exchanges those SDP blobs before any video flows.
Plain-English First
Imagine you and a friend want to pass notes in class without the teacher (a server) reading every single one. First you both tell the teacher where you're sitting so you can find each other — that's signaling. Then you pass notes directly between desks without the teacher in the middle — that's WebRTC. The teacher only helped you locate each other; after that, you're talking peer-to-peer. WebRTC is just the browser's built-in ability to let two devices talk directly — sharing video, audio, or any data — without a middleman relaying every byte.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Every time you hop on a Google Meet call, share your screen on Discord, or do a live video consultation with a doctor, there's a real-time peer-to-peer communication layer quietly doing enormous amounts of work beneath the surface. That layer is WebRTC. It's baked into every major browser, it's free, and it's one of the most architecturally complex systems you'll encounter in web development — precisely because it has to punch through firewalls, negotiate codecs, handle network jitter, and do all of this in under a second. Understanding it at the component level is the difference between cargo-culting a tutorial and actually shipping a reliable product.
The problem WebRTC solves is deceptively hard: two browsers sitting behind separate corporate firewalls, NAT routers, and ISPs need to send live media to each other with sub-200ms latency. Traditional HTTP request-response doesn't work — there's no persistent bidirectional channel, and routing every video frame through your server would be catastrophically expensive at scale. WebRTC solves this by giving browsers a standardized API to discover each other's network addresses, agree on a common media format, and then open a direct encrypted UDP channel — all without you writing a single line of native socket code.
By the end of this article you'll be able to reason through the entire WebRTC handshake from first principles: what ICE candidates are and how they're gathered, what SDP actually encodes and why it matters, when STUN is enough and when you absolutely need TURN, how the DataChannel differs from MediaStream tracks, and what goes wrong in production when corporate proxies eat your UDP packets. You'll also walk away with annotated code showing the full offer-answer exchange and a comparison table to help you choose the right ICE topology for your use case.
How WebRTC Actually Connects Two Browsers Without a Server
WebRTC is a browser-to-browser protocol for real-time audio, video, and data transfer — no plugins, no central media server. The core mechanic is peer-to-peer: once a connection is established, media flows directly between clients. But the setup path is anything but direct. It relies on ICE (Interactive Connectivity Establishment) to discover the best network path, STUN servers to find your public IP and port, and TURN servers to relay traffic when NAT or firewalls block direct connections. SDP (Session Description Protocol) negotiates codecs, resolutions, and encryption keys before any packet is sent.
In practice, WebRTC works in three phases: signaling (exchange SDP offers/answers via your own server — WebRTC doesn't define this), ICE candidate gathering (STUN probes to find reachable addresses), and connectivity checks (pairs of candidates are tested until one works). The key property that matters in production is that ICE can take 2–5 seconds to complete, and if TURN relay is required, latency jumps by 50–100 ms and bandwidth costs spike. You cannot skip STUN/TURN configuration and expect reliable connections.
Use WebRTC when you need sub-500 ms latency for voice/video, screen sharing, or real-time data channels. It's the only browser-native option for peer-to-peer media. In real systems, it powers Zoom-like conferencing, live streaming, and remote desktop tools. The trade-off is complexity: you must run your own signaling server, configure STUN/TURN, and handle fallback logic when ICE fails. Without proper TURN infrastructure, up to 15% of connections will fail in enterprise networks with symmetric NAT.
⚠ STUN Is Not Optional
STUN only works for cone NAT — symmetric NAT requires TURN. Skipping TURN means 10–20% of users will fail to connect, silently.
📊 Production Insight
A video conferencing app deployed without TURN servers saw 18% of calls fail on corporate networks with symmetric NAT.
The symptom: ICE candidates never reach 'connected' state; the connection times out after 30 seconds with no media flowing.
Always provision at least one TURN server per region, and set ICE transport policy to 'relay' for enterprise customers.
🎯 Key Takeaway
ICE is not a single connection attempt — it's a combinatorial search that can take seconds.
STUN is free but insufficient for symmetric NAT; TURN is mandatory for production reliability.
SDP negotiation must happen over your own signaling channel — WebRTC provides no built-in signaling.
ICE: Interactive Connectivity Establishment
ICE is the core negotiation protocol that determines the best path for media between two peers. It collects a list of candidate network addresses (local, STUN-reflexive, TURN-relayed) and tests them in order of priority. The goal is to find a pair that works — typically the fastest direct path wins. ICE handles network changes, NAT rebinding, and even multi-homed hosts. Without ICE, you'd need to manually configure every network topology.
New ICE candidate: sdp=candidate:1 1 UDP 2122252543 192.168.1.10 54321 typ host, sdpMid=audio, sdpMLineIndex=0
📊 Production Insight
ICE trickle (sending candidates as they're discovered) reduces connection time by 30-50%.
But if signaling channel is slow, batch candidates into a single message.
Rule: always implement trickle ICE for sub-500ms handshakes.
🎯 Key Takeaway
ICE tries candidates by priority, not speed.
Host candidates fail behind NAT — STUN and TURN are essential fallbacks.
Monitor iceConnectionState transitions in production.
thecodeforge.io
Webrtc Explained
STUN: Session Traversal Utilities for NAT
STUN is a lightweight protocol that lets a client discover its public IP address and port as seen from the internet. The client sends a binding request to a STUN server, which responds with the observed source address. This gives you a 'reflexive candidate' that can be used by the remote peer to reach you. STUN works for most home NATs but fails under symmetric NATs (common in corporate firewalls) because the port mapping changes per destination.
STUN server response: 203.0.113.42:3478 (public IP for NAT traversal)
📊 Production Insight
STUN requests can be rate-limited or blocked by firewalls.
Always monitor stun.requests.sent vs received stats.
Rule: if more than 20% of STUN requests time out, add a TURN fallback.
🎯 Key Takeaway
STUN solves simple NAT but fails on symmetric NAT.
Use STUN only as a first attempt, never as the sole path.
A public STUN server is fine for dev, but deploy your own for production.
TURN: Traversal Using Relays around NAT
TURN is the last resort: it relays all media through a server on the public internet. When direct P2P fails (ICE reaches the end of candidate list without success), one peer connects to a TURN server and the other connects to the same relay. The TURN server forwards packets between them. This adds latency (~100-200ms extra) and server bandwidth costs, but it guarantees connectivity even under symmetric NATs or firewall blocks.
ICE servers configured: STUN (stun.l.google.com:19302) and TURN (turn.example.com:3478)
📊 Production Insight
TURN servers are expensive at scale — 1Gbps relay costs ~$1000/month.
Optimize by only enabling TURN for users who fail STUN (ICE restart).
Rule: use a TURN server close to your users to minimize latency.
🎯 Key Takeaway
TURN is a necessary evil: guarantees connectivity but adds latency and cost.
Always have a TURN fallback for enterprise users.
Prefer TCP/443 TURN for firewall-friendliness.
SDP: Session Description Protocol
SDP describes the media session: codecs (H264, VP8, Opus), encryption keys (DTLS fingerprint), bandwidth, and network parameters. It's a plaintext format that both peers exchange via signaling. The 'offer' contains the caller's capabilities; the 'answer' contains the callee's intersection. SDP is not a transport protocol — it's a negotiation contract. Once agreed, media flows using the chosen parameters.
io/thecodeforge/webrtc/SdpHandler.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package io.thecodeforge.webrtc;
import org.webrtc.SessionDescription;
publicclassSdpHandler {
publicstaticvoidonOfferReceived(String sdp) {
SessionDescription remoteDesc = newSessionDescription(SessionDescription.Type.OFFER, sdp);
// Set remote description, then create answerSystem.out.println("Received SDP offer with codecs: " + extractCodecs(sdp));
}
privatestaticStringextractCodecs(String sdp) {
// Parse SDP lines like a=rtpmap:96 VP8/90000StringBuilder sb = newStringBuilder();
for (String line : sdp.split("\n")) {
if (line.startsWith("a=rtpmap:")) {
sb.append(line.substring(9)).append(", ");
}
}
return sb.length() > 0 ? sb.substring(0, sb.length() - 2) : "none";
}
}
Output
Received SDP offer with codecs: VP8/90000, H264/90000, opus/48000
📊 Production Insight
SDP lines can exceed MTU (1500 bytes) causing fragmentation or truncation.
Always send SDP over a reliable channel (WebSocket, not UDP).
Rule: if media never starts, compare SDP offers — incompatible codecs are the top cause.
🎯 Key Takeaway
SDP is the negotiation contract, not the media itself.
Codec mismatch is silent — always log the SDP on both sides.
Prefer to set codec preferences explicitly, not rely on browser defaults.
Signaling: The Hidden Handshake
Signaling is the out-of-band exchange of session control messages (SDP offers/answers and ICE candidates) before the peer-to-peer connection exists. WebRTC does not define signaling — you use your own channel: WebSocket, HTTP, XMPP, or even carrier pigeon. The only requirement is that it's fast and reliable. Signaling is often where developers trip up: missing a candidate, reordering messages, or not handling multiple calls.
Signaling is the number one source of WebRTC bugs: message order, lost candidates, stale sessions.
Use a message queue with acknowledgements.
Rule: never trust signaling to be in order — buffer candidates until SDP exchange is done.
🎯 Key Takeaway
Signaling is not part of WebRTC standard — you must build it.
Reliability and ordering are your responsibility.
Always send ICE candidates after SDP exchange to avoid race conditions.
DataChannel: Beyond Audio/Video
DataChannel enables arbitrary data transfer between peers (files, game state, chat) with configurable reliability. It's built on SCTP over DTLS. You can choose 'reliable' (TCP-like) or 'unreliable' (UDP-like with ordered/unordered). DataChannel is ideal for low-latency game inputs or real-time collaboration where every packet matters.
DataChannel buffers can grow unbounded if the remote is slow — set maxRetransmits or maxPacketLifeTime.
On mobile, DataChannel may not survive app background state.
Rule: use ordered unreliable for game inputs (low latency, occasional drops OK), reliable for file transfers.
🎯 Key Takeaway
DataChannel gives you raw P2P data, not just media.
Choose reliability vs speed based on use case.
Monitor DataChannel state transitions to detect stalls.
Why Your WebRTC Call Keeps Dropping: The NAT Debugging Nightmare
You've got ICE, STUN, and TURN working on paper. Great. But your production WebRTC app still drops calls when users are on corporate VPNs or carrier-grade NAT. Here's the dirty secret: NAT traversal isn't a binary pass/fail. It's a probabilistic clusterf***.
STUN works for about 70% of clients. That's the easy part. The remaining 30% have symmetric NAT, where your public IP:port binding changes per destination. STUN can't see this because it only talks to one server. Your ICE agent wastes seconds trying candidates that will never work. Meanwhile, the user sees "Connecting..." and rage-refreshes.
The fix? Run multiple STUN servers on different subnets. Google's free STUN (stun:stun.l.google.com:19302) is great until it rate-limits you. Deploy your own STUN behind anycast IPs. Also, implement ICE restarts when media fails after a successful connection. A network change mid-call (WiFi to cellular) invalidates your previous NAT mapping. Restart ICE immediately, don't wait for the 30-second timeout.
Aggressive nomination in ICE can mask connectivity issues during initial handshake but fail under load. Use regular nomination. It tests all candidates before selecting the final pair. Yes, it's slower. Yes, it saves your call quality when networks degrade.
NatTriage.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// io.thecodeforge — system-design tutorial
import subprocess
import json
defcheck_nat_type(stun_host: str) -> dict:
"""QuickNAT type detection using STUN response.
Returns dict with mapped port and changed address fields."""
cmd = [
"stunclient", "--mode", "full",
"--local-port", "19302", stun_host, "3478"
]
try:
output = subprocess.check_output(cmd, timeout=5).decode()
lines = output.strip().split("\n")
info = {}
for line in lines:
if"Mapped address"in line:
info["map_addr"] = line.split(":")[1].strip()
if"Changed address"in line:
info["changed_addr"] = line.split(":")[1].strip()
# Symmetric NAT: mapped port != changed port
info["is_symmetric"] = (
info.get("map_addr") != info.get("changed_addr")
)
return info
except subprocess.TimeoutExpired:
return {"error": "STUN timeout - likely blocked UDP"}
result = check_nat_type("stun.l.google.com")
print(json.dumps(result, indent=2))
Output
{
"map_addr": "203.0.113.50:12345",
"changed_addr": "203.0.113.50:12346",
"is_symmetric": true
}
⚠ Production Trap:
Don't rely on a single STUN server. Google's free server returns ice-candidates based on its view of your network. If your user is behind carrier-grade NAT (common in mobile), that mapping is useless for a different STUN server. Run two STUN servers from different cloud regions and compare the mapped addresses.
🎯 Key Takeaway
NAT traversal isn't a binary pass/fail. Symmetric NAT requires TURN relay immediately—don't waste time on ICE candidates that will always fail.
SDP Bloat: The Silent Killer of WebRTC Performance
Your SDP offer is 15KB. It takes 800ms to parse on the receiving peer. Why? Because every browser vendor includes every codec, every packetization mode, and every extension they've ever shipped. Chrome alone advertises 18 H.264 profiles, 4 VP8 configurations, and 3 Opus bitrates. The remote peer has to filter through this garbage to find what actually works.
This isn't just a latency issue. Large SDP causes fragmentation in WebSocket signaling messages, which leads to retransmission and reordering. Your ICE candidates arrive after SDP because they're in separate attributes—the peer starts gathering candidates before it knows which codecs to use, wasting resources.
The fix is SDP pruning. Before sending an offer, strip everything except the codecs you actually intend to use. If you only need VP8 at 30fps, remove H.264 entirely. Remove redundant rtpmap attributes. Many media servers (Janus, Mediasoup) do this automatically. If you're building peer-to-peer without a media server, you must implement it yourself.
Also, use "rollback" semantics in the createOffer/Answer flow. Modern browsers support it. Rejecting an old offer and sending a new one is atomic—no stale SDP in flight. And for god's sake, enable RTCP-mux and bundle. Reduces candidate pairs from n^2 to n. Your ICE agent will thank you.
SdpPruner.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// io.thecodeforge — system-design tutorial
import re
defprune_sdp(sdp: str, keep_codecs: set) -> str:
"""Remove unwanted codec lines fromSDP.
Keeps only codecs in { 'VP8', 'opus' } for example."""
lines = sdp.split("\r\n")
pruned = []
in_media = False
codec_map = {}
for line in lines:
if line.startswith("m="):
in_media = True# Extract payload types from media line
parts = line.split()
# First 3 parts are: m=audio/rtp, port, proto
payloads = parts[3:]
pruned.append(parts[0]) # placeholder
codec_map[line] = payloads
elif in_media:
if line.startswith("a=rtpmap:"):
# rtpmap:96 VP8/90000
match = re.search(r":(\d+) (.+?)/", line)
if match:
pt, codec = match.groups()
if codec notin keep_codecs:
continue
pruned.append(line)
else:
pruned.append(line)
return"\r\n".join(pruned)
sdp_raw = open("offer.sdp", "r").read()
pruned = prune_sdp(sdp_raw, {"VP8", "opus", "rtx"})
print(pruned[:500])
Use the 'max-audio-bandwidth' constraint in RTCRtpSender.setParameters() to cap Opus bitrate at 64kbps. Most browsers default to 128kbps. Cuts SDP size by 20% and bandwidth by 50% with no perceptible quality loss for voice chats.
🎯 Key Takeaway
SDP bloat is real. Prune codecs before signaling. Smaller SDP = faster connection = happier users.
How WebRTC Establishes a Connection (Step by Step)
Skip the magic. Here's the raw sequence your browser executes when you click "start call." First, your app sends an SDP offer over your signaling channel—WebSocket, HTTP, carrier pigeon, whatever. That SDP blob contains your codec preferences, ICE candidates, and crypto fingerprints. The remote peer sends back an SDP answer. Now both sides have each other's session descriptions and public candidate addresses.
Then ICE kicks in. Each browser gathers candidates: local IPs, STUN-mapped public IPs, and TURN relay addresses. They prioritize by connectivity cost. Local LAN? Instant. STUN? Fast. TURN? Last resort—you're paying for relay bandwidth.
Connectivity checks start immediately. STUN binding requests fire between every candidate pair. First pair to return a successful response wins. That's your active connection. Everything after that—DTLS-SRTP keying, codec negotiation—runs on that established pair. Your first audio packet flies maybe 200ms after the SDP answer arrived.
Your signaling channel is a single point of failure. If your WebSocket drops mid-negotiation, both sides hang. Always implement retry logic with exponential backoff.
🎯 Key Takeaway
WebRTC connections are a race between candidate pairs. First STUN check to succeed wins.
thecodeforge.io
Webrtc Explained
The Solution: SFU (Selective Forwarding Unit)
Mesh architectures work for 3 people in a Google Meet. Beyond that, your browser melts trying to encode 9 separate video streams. That's where the SFU enters—the backbone of every production WebRTC deployment. Zoom, Discord, Twitch—they all run on SFUs.
An SFU is a server that receives one upstream from each participant and selectively forwards it to everyone else. No decoding, no transcoding. Just packet switching. Your browser sends one video stream to the SFU. The SFU copies that stream to every other participant. You encode once, they receive as many streams as they want.
The magic is selective forwarding. The SFU doesn't send 4K to someone on mobile with bad signal. It forwards the lowest bitrate simulcast layer. It drops packets from a muted speaker entirely. No wasted bandwidth. No client-side encoding hell.
SFUs scale horizontally. Need 10,000 participants? Spin up 50 SFU nodes behind a load balancer. Each node handles 200 people. Your WebRTC app just talks to the SFU—one connection, one encoding burden.
Never build your own SFU. Use LiveKit, Mediasoup, or Janus. They've solved simulcast, congestion control, and reconnection edge cases. You'll spend 6 months fixing what they fixed in 2019.
🎯 Key Takeaway
An SFU shifts the encoding burden from N clients to 1 server. Scale horizontally, not vertically.
Why LiveKit Specifically
You could roll Janus, mediasoup, or even raw WebRTC with libwebrtc. Don't. LiveKit is the production shortcut your team needs. Here's why.
First, it's Go-based. One binary, zero dependencies. Deploy it on a t3.medium and it handles 500 concurrent rooms without flinching. Memory footprint? ~50MB per 100 participants. Compare that to Janus which needs a Redis cluster for state management.
Second, the WebRTC layer is abstracted. You don't write SDP parsing code. You don't configure STUN/TURN servers manually. LiveKit auto-discovers your network setup. Their clients (React, iOS, Android) expose Room, Participant, and Track objects. That's it. Your app sends audio, receives video, and handles disconnects with 3 lines of code.
Third, the ecosystem. Webhooks for recording, ingress for RTMP feeds, egress for file output. You can live-stream a WebRTC call to YouTube without writing a single transcoding pipeline. Their data channel API surfaces real-time metadata—reactions, chat, whiteboard state—over the same WebRTC connection.
Skip the academic research phase. LiveKit ships production-ready SFU logic, client libraries, and monitoring dashboards. Your job is to build the UI, not debug ICE failures.
LiveKitRoom.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — system-design tutorial
from livekit import api
client = api.LiveKitAPI(
url="wss://my-livekit-server:7880",
api_key="API4abc123",
api_secret="sk_secretxyz"
)
room = client.create_room(
name="team-sync",
max_participants=50,
empty_timeout=300
)
# Participant connects via frontend SDK:# await Room.connect(token, { audio: true, video: true })# Track is auto-published to the SFUprint(f"Room {room.name} ready, SID: {room.sid}")
Output
Room team-sync ready, SID: RM_abc123
SFU allocated: livekit-node-3:7880
WebRTC negotiation complete in 187ms
⚠ Production Trap:
LiveKit tokens expire. Set them to 1 hour and refresh via your backend. A 24-hour token leaking is free access to every active room. Implement room-level ACLs in your token generation.
🎯 Key Takeaway
LiveKit removes SDP boilerplate and SFU ops. It's a drop-in WebRTC backend with first-class React support.
● Production incidentPOST-MORTEMseverity: high
ICE Timeout in Production Video Service
Symptom
Approximately 30% of enterprise users saw a black screen after accepting a call. The WebRTC internals showed "ICE failed, add a TURN server" in about:webrtc logs.
Assumption
STUN would suffice for all users since the service ran on the public internet. TURN was deemed unnecessary overhead.
Root cause
The corporate firewall blocked all UDP ports except 443 and 53. STUN servers (typically on port 3478) were unreachable, so ICE could not discover the public endpoint. Without a TURN fallback, the connection never established.
Fix
Deployed a TURN server (coturn) on port 443 over TCP/TLS, configured in the ICE servers list as a fallback. Also added a healthcheck on the STUN endpoint to alert when blocked.
Key lesson
Always provide a TURN relay as fallback for enterprise users.
Test WebRTC behind restrictive firewalls before production launch.
Monitor ICE connection statistics (stats.stun_requests_sent vs received) to detect blocking early.
Production debug guideSymptom → Action for common production failures4 entries
Symptom · 01
ICE candidate gathering never completes (stuck in 'checking')
→
Fix
Open chrome://webrtc-internals, check 'iceConnectionState'. If no candidates, verify STUN/TURN URLs are reachable from client network.
Symptom · 02
STUN request succeeds but ICE fails
→
Fix
Check if both peers can reach each other's candidates. Try disabling UDP in TURN config; some networks only allow TCP.
Symptom · 03
TURN relay used but latency spikes to 500ms+
→
Fix
Verify TURN server location is geographically close to both peers. Consider split-tunnel DNS to avoid routing through a central VPN.
Symptom · 04
SDP exchange succeeds but no media flows
→
Fix
Check codec priorities. Force H264 if VP8 is failing on a device. Also verify ICE restart logic works after network changes.
★ WebRTC ICE/STUN/TURN Debugging Cheat SheetQuick commands and checks for common WebRTC production issues
ICE timeout−
Immediate action
Open chrome://webrtc-internals and click 'GetAndSetRTCConfiguration'
Commands
Check 'iceConnectionState' — should be 'completed' not 'failed'
nslookup stun.l.google.com to verify DNS resolution
Fix now
Add a TURN server to iceServers: { urls: 'turn:your-server.com:3478', username: '...', credential: '...' }
No media after connection+
Immediate action
Check 'trackStats' in chrome://webrtc-internals
Commands
Verify both peers have audio/video tracks added to RTCPeerConnection
Check if codec negotiation failed: look for 'codecName' in SDP
Fix now
Force codec order: use RTCRtpTransceiver.setCodecPreferences
Stun request fails+
Immediate action
Try telnet from client machine to STUN server port 3478
Commands
telnet stun.l.google.com 3478
If fails, check firewall allows UDP 3478 or switch to TCP
Fix now
Use a STUN server on port 443 (e.g., stun:stun.voiparound.com:443)
WebRTC Component Comparison
Component
Role
When to Use
Production Pitfall
ICE
Path discovery and selection
Every WebRTC connection
Timeout if no working candidate pair found
STUN
NAT reflection
Home networks, simple NAT
Fails on symmetric NAT
TURN
Media relay
Corporate firewalls, symmetric NAT
High latency and bandwidth cost
SDP
Session negotiation
Before media starts
Codec mismatch stops media silently
DataChannel
P2P data transfer
File sharing, gaming, chat
Buffer overflow if reliability not configured
⚙ Quick Reference
11 commands from this guide
File
Command / Code
Purpose
iothecodeforgewebrtcIceCandidate.java
public class IceCandidateHandler {
ICE
iothecodeforgewebrtcStunQuery.java
public class StunQuery {
STUN
iothecodeforgewebrtcTurnConfig.java
public class TurnConfig {
TURN
iothecodeforgewebrtcSdpHandler.java
public class SdpHandler {
SDP
iothecodeforgewebrtcSignalingChannel.java
public class SignalingChannel {
Signaling
iothecodeforgewebrtcDataChannelExample.java
public class DataChannelExample {
DataChannel
NatTriage.py
def check_nat_type(stun_host: str) -> dict:
Why Your WebRTC Call Keeps Dropping
SdpPruner.py
def prune_sdp(sdp: str, keep_codecs: set) -> str:
SDP Bloat
IcedStateMachine.py
class IcedConnection:
How WebRTC Establishes a Connection (Step by Step)
SfuRouter.py
class SelectiveForwardingUnit:
The Solution
LiveKitRoom.py
from livekit import api
Why LiveKit Specifically
Key takeaways
1
WebRTC combines ICE, STUN, TURN, and SDP to establish a P2P connection.
2
Always provide a TURN fallback for enterprise production environments.
3
Debug ICE failures by checking candidate types and network reachability.
4
SDP codec mismatch is silent
log and compare SDP offers on both sides.
5
Signaling must be reliable and ordered; buffer ICE candidates until SDP exchange completes.
Common mistakes to avoid
3 patterns
×
No TURN fallback for enterprise users
Symptom
30-50% of corporate users can't connect — ICE fails silently, no error visible.
Fix
Add a TURN server (coturn) on TCP/443 with TLS, ensure iceServers list includes turn: and stun: entries.
×
Sending ICE candidates before SDP exchange
Symptom
ICE negotiation succeeds but media never flows — remote peer discards candidates received before remote description was set.
Fix
Queue ICE candidates until setRemoteDescription() is called, then flush them.
×
Assuming codec support is symmetric
Symptom
Audio works but video doesn't — browser A supports H264 only, browser B VP8 only.
Fix
Use RTCRtpTransceiver.setCodecPreferences to specify fallback order, or force a common codec server-side.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain the ICE process in WebRTC. What happens when a STUN request fail...
Q02SENIOR
How would you debug a situation where two WebRTC peers successfully conn...
Q03SENIOR
What is the difference between a STUN and TURN server? When would you us...
Q01 of 03SENIOR
Explain the ICE process in WebRTC. What happens when a STUN request fails?
ANSWER
ICE gathers candidates (host, reflexive, relay) and tests them in priority order. If STUN fails (e.g., timeout or error), that candidate is discarded. ICE continues with other candidates. If all candidates fail, the connection fails unless a TURN relay candidate is available. In production, you'd fall back to TURN after a timeout (e.g., 5 seconds) to avoid indefinite wait.
Q02 of 03SENIOR
How would you debug a situation where two WebRTC peers successfully connect but no audio is transmitted?
ANSWER
First check chrome://webrtc-internals for trackStats and iceConnectionState. Look for 'inbound-rtp' and 'outbound-rtp' entries — if missing, no media is being sent/received. Compare SDP offers: ensure codecs match (a=rtpmap lines). Check if audio constraints were set correctly (audio: true). If media still doesn't flow, verify that remote stream tracks are attached to the correct <audio> element.
Q03 of 03SENIOR
What is the difference between a STUN and TURN server? When would you use each?
ANSWER
STUN discovers your public IP (simple NAT traversal); TURN relays all media when direct P2P fails. Use STUN for home users behind cone NAT. Use TURN for corporate firewalls, symmetric NATs, or when ICE times out. TURN adds ~150ms latency and costs bandwidth, so use it only as a fallback.
01
Explain the ICE process in WebRTC. What happens when a STUN request fails?
SENIOR
02
How would you debug a situation where two WebRTC peers successfully connect but no audio is transmitted?
SENIOR
03
What is the difference between a STUN and TURN server? When would you use each?
SENIOR
FAQ · 3 QUESTIONS
Frequently Asked Questions
01
What is WebRTC Explained in simple terms?
WebRTC Explained is a fundamental concept in System Design. Think of it as a tool — once you understand its purpose, you'll reach for it constantly. In practice, WebRTC is the technology that lets browsers talk directly to each other for video calls, file sharing, and real-time data — all without a central media server.
Was this helpful?
02
Why does my WebRTC app work in dev but fail in production?
Most likely a firewall blocking UDP ports or symmetric NAT breaking STUN. Use a TURN server on TCP/443 as a fallback, and always test on real corporate networks before launch.
Was this helpful?
03
What's the cheapest way to deploy a TURN server?
Use coturn on a cheap VPS near your users. A single core with 1GB RAM can handle ~50 concurrent relays. For scale, use a managed service like Twilio or Metered.ca that charges per GB relayed.