WebRTC#
WebRTC is the browser standard for sending audio, video, and arbitrary data peer-to-peer over UDP. It bundles four hard problems into one API: discovering reachable network paths through NAT, negotiating codecs, encrypting media, and giving JavaScript a handle to start and stop streams. Understanding WebRTC ICE STUN TURN is the foundation for any real-time media system design interview.
flowchart LR
A([Browser A]) -->|1 SDP offer + ICE| Sig[Signaling server]
Sig -->|2 forward| B([Browser B])
B -->|3 SDP answer + ICE| Sig
Sig -->|4 forward| A
A ==>|5 DTLS-SRTP media over UDP| B
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
class A,B client;
class Sig service;
The signaling server only carries setup messages. Once ICE finds a working path, real audio and video bytes flow directly between the two browsers without touching your backend. This is the property that makes WebRTC cheap to scale for one-to-one calls and the reason Discord, Google Meet, and Zoom started with WebRTC as the wire protocol.
Why peer-to-peer media needs NAT traversal#
Almost every home and office network sits behind NAT. A laptop sees 192.168.1.42 for itself, but the public internet sees the router's external IP with a translated port. The two peers cannot just exchange their LAN addresses; those addresses are not routable on the open internet. WebRTC solves this with ICE, a framework for discovering every plausible address pair and trying them in order until one works.
The four kinds of ICE candidates#
flowchart TB
Peer([Peer])
Peer --> Host[Host candidate<br/>LAN IP, eg 192.168.1.42]
Peer --> Srflx[Server-reflexive<br/>public IP via STUN]
Peer --> Prflx[Peer-reflexive<br/>learned during connectivity check]
Peer --> Relay[Relay candidate<br/>via TURN server]
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
class Peer client;
class Host,Srflx,Prflx edge;
class Relay external;
ICE pairs every local candidate with every remote candidate and runs STUN binding-request pings on each pair. The first pair that succeeds wins. Host pairs win when both peers are on the same LAN; server-reflexive pairs win across most home networks; relay pairs win when nothing else can punch through.
STUN, in one sentence#
A STUN server is a stateless UDP echo that tells you what your public IP and port look like from outside. Cost: near zero. Google runs stun.l.google.com:19302 for free, which is why most tutorials use it.
TURN, the expensive fallback#
When both peers are behind symmetric NAT (common on corporate networks and some mobile carriers), STUN cannot help. A TURN server accepts the media stream from one peer and forwards it to the other, like a proxy that speaks the WebRTC media protocol. Every byte of audio and video then flows through your TURN infrastructure, which is why TURN is the line item that scares finance: a 1080p video call at 2.5 Mbps costs roughly 1 GB per hour per direction at the TURN box.
Roughly 10 to 20 percent of WebRTC calls in the wild need TURN. Plan for it.
The bigger picture#
flowchart TB
subgraph Peers
A([Browser A])
B([Browser B])
end
subgraph Helpers
Sig[Signaling<br/>WebSocket]
STUN[STUN server]
TURN[TURN server]
end
A -->|SDP + ICE| Sig
B -->|SDP + ICE| Sig
A -->|what is my public IP| STUN
B -->|what is my public IP| STUN
A -.->|fallback relay| TURN
TURN -.-> B
A ==>|direct UDP, common path| B
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
class A,B client;
class Sig,STUN service;
class TURN external;
DataChannel vs MediaStream#
WebRTC carries two payload types over the same connection:
| Channel | Protocol | Use cases |
|---|---|---|
MediaStream |
SRTP (audio/video frames) | Camera, microphone, screen share |
DataChannel |
SCTP over DTLS | Game state, file transfer, whiteboard cursor positions, chat |
Both ride on the same ICE-negotiated UDP socket and share its DTLS encryption. Google Docs collaborative cursors and the cursor sync in Figma rooms use DataChannel; Zoom and Meet use MediaStream for AV.
When pure P2P stops working#
Mesh peer-to-peer fan-out scales to about four or five participants. Each peer must encode the camera once but upload that stream to every other peer, so a six-person call asks your laptop to upload five copies of 720p video. Past that point, every major product routes through a Selective Forwarding Unit: a server that accepts one upload per peer and forwards copies to subscribers without re-encoding.
- Zoom: SFU per data center; selective layer forwarding based on what each client requests
- Google Meet: SFU with simulcast (each peer sends three quality layers)
- Discord voice: SFU per voice region; Opus audio only on the low-rate path
- Twilio Video, Daily, LiveKit: SFU-as-a-service
You still use WebRTC on the wire: the SFU speaks ICE, DTLS-SRTP, and SDP. It just terminates the media instead of forwarding raw IP.
Security: DTLS-SRTP, not optional#
WebRTC mandates encryption. Media frames are SRTP with keys derived from a DTLS handshake that runs over the same ICE-negotiated UDP socket. There is no plaintext mode. Browsers will not even let you open a getUserMedia stream from an insecure context (HTTP without TLS), which forces the entire pipeline to be encrypted end-to-end between peers.
The encryption is point-to-point. If you route through an SFU, the SFU sees plaintext frames (it has the SRTP keys). End-to-end encrypted SFUs exist (Zoom's E2EE mode, Meet end-to-end calls) but require an extra layer of frame encryption on top of SRTP.
What to remember#
- WebRTC is media plus a connection-setup protocol; signaling is out of scope.
- ICE gathers candidates, STUN discovers public IPs, TURN relays when nothing else works.
- UDP is the transport; reliability and ordering are negotiated per channel.
- Mesh dies at five peers; everything bigger uses an SFU.
- Encryption (DTLS-SRTP) is non-negotiable.
WebRTC bundles four problems into one browser API: NAT traversal, codec negotiation, congestion control, and encryption. This deep dive on WebRTC ICE STUN TURN walks through how a peer connection is established, what each helper server does, and where you start paying the bill at scale.
Architecture overview#
flowchart TB
subgraph Browser_A[Browser A]
PCA[RTCPeerConnection]
DCa[DataChannel]
MSa[MediaStream]
end
subgraph Browser_B[Browser B]
PCB[RTCPeerConnection]
DCb[DataChannel]
MSb[MediaStream]
end
subgraph App_Infra[App infrastructure]
Sig[Signaling service<br/>WebSocket]
STUN[STUN server<br/>port 3478]
TURN[TURN server<br/>port 3478, 5349]
end
PCA <-->|SDP + ICE| Sig
PCB <-->|SDP + ICE| Sig
PCA -->|binding request| STUN
PCB -->|binding request| STUN
PCA -.->|relay path, fallback| TURN
TURN -.-> PCB
PCA ==>|DTLS-SRTP direct path| PCB
DCa -.- PCA
MSa -.- PCA
DCb -.- PCB
MSb -.- PCB
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
class PCA,PCB,DCa,DCb,MSa,MSb client;
class Sig,STUN service;
class TURN external;
Three categories of server sit between the peers, each with a distinct lifecycle:
- Signaling: short-lived; carries SDP + ICE candidates for a few seconds during setup; idle for the rest of the call.
- STUN: stateless; each binding request is one round trip; can be hosted free or on commodity infra.
- TURN: stateful; pins one allocation per session; bills CPU + bandwidth for the entire call.
The signaling layer is the part WebRTC deliberately does not standardize. Implementers are free to use WebSocket, HTTP long-poll, SIP-over-WebSocket, Firebase Realtime Database, MQTT, or hand-rolled long-polling. The only constraint is that both peers reach the signaling service before they can find each other.
The connection lifecycle#
sequenceDiagram
participant A as Browser A
participant Sig as Signaling
participant STUN as STUN server
participant TURN as TURN server
participant B as Browser B
A->>A: createOffer (SDP)
A->>STUN: gather server-reflexive
A->>TURN: allocate relay (if configured)
A->>Sig: send SDP offer
Sig->>B: forward offer
B->>B: setRemoteDescription, createAnswer
B->>STUN: gather candidates
B->>Sig: send SDP answer
Sig->>A: forward answer
par Trickle ICE
A-->>Sig: ICE candidate (host)
A-->>Sig: ICE candidate (srflx)
A-->>Sig: ICE candidate (relay)
Sig-->>B: forward each
and
B-->>Sig: ICE candidate (host)
B-->>Sig: ICE candidate (srflx)
Sig-->>A: forward each
end
A->>B: connectivity checks (STUN binding)
B->>A: connectivity checks (STUN binding)
A->>B: DTLS handshake
A->>B: SRTP media flowing
Three signals flow through the signaling channel: the SDP offer (capabilities + initial candidates), the SDP answer (negotiated codec set + initial candidates), and a stream of trickled ICE candidates. After ICE picks a working pair, all media bytes leave the signaling path and ride DTLS-SRTP directly between peers.
ICE: candidate gathering and prioritization#
ICE (Interactive Connectivity Establishment, RFC 8445) is an algorithm, not a wire protocol. Each peer enumerates four candidate types:
| Candidate type | Source | Typical priority |
|---|---|---|
host |
Every local IP on every interface | Highest |
srflx (server-reflexive) |
STUN binding response: public IP and port | High |
prflx (peer-reflexive) |
Learned from incoming connectivity checks | Medium |
relay |
TURN server allocation | Lowest |
Candidates carry a 32-bit priority computed from candidate type, local preference, and component (RTP vs RTCP). The pair check order is host x host > host x srflx > srflx x srflx > anything x relay. The first working pair is nominated and used.
A typical home-network call ends up on srflx x srflx. Both peers connected to home Wi-Fi behind cone NATs each have a public IP/port mapping that STUN exposed; the peers send packets straight to those addresses and the NAT box accepts inbound traffic because it sees prior outbound packets to the same address (hole punching).
STUN: when it works, when it doesn't#
STUN (RFC 8489) is six message types over UDP. The only one most code uses is Binding Request. The server replies with the source IP and port it saw on the request, which is the peer's public mapping for that 5-tuple.
STUN works when the NAT is one of three behaviors: full-cone, restricted-cone, or port-restricted-cone. It fails when the NAT is symmetric: a NAT that allocates a new external port for every destination IP. In symmetric NAT, the port the STUN server saw is not the port that will work for the peer, because the peer is a different destination IP.
How often does this happen? Roughly:
- 70 to 80 percent of home networks: cone NAT, STUN works
- 10 to 15 percent: corporate firewalls allowing UDP, STUN works
- 5 to 10 percent: symmetric NAT (some carrier-grade NAT, hotel Wi-Fi, restrictive corporate)
- 1 to 5 percent: UDP blocked entirely
The last two categories need TURN.
TURN: the relay of last resort#
TURN (RFC 8656) is a relay protocol implemented as an extension to STUN. The client sends an Allocate request; the server reserves a public IP and port. From then on, anything the client sends to that allocation, the TURN server forwards to a configured remote peer, and vice versa. The two peers never talk directly; they each talk to the TURN box.
flowchart LR
A([Browser A]) -->|relay channel| T[TURN<br/>public IP]
T -->|relay channel| B([Browser B])
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
class A,B client;
class T external;
TURN supports three transports: UDP (default), TCP (for networks that block UDP), and TLS (port 5349; the most permissive option because it looks like HTTPS). Many corporate networks allow only outbound 443; TURN-over-TLS on 443 is the only way these clients can join a call.
Cost economics. A 1080p video call at 2.5 Mbps that runs entirely on TURN consumes 2.5 Mbps in and 2.5 Mbps out at the TURN server for one hour: roughly 2.25 GB total. At commercial relay rates ($0.40 to $1.00 per GB), one hour-long 1080p call relayed for both directions costs more than $1. Multiply by ten million minutes per day and TURN becomes a real line item. This is why every serious deployment runs its own coturn cluster and uses commercial TURN-as-a-service (Twilio NTS, Xirsys) only as a backup region.
SDP: the lingua franca for media capabilities#
SDP (Session Description Protocol, RFC 8866) is a 1990s text format that describes what a peer can send and receive. A trimmed offer looks like:
v=0
o=- 4611732884 2 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111 103
c=IN IP4 0.0.0.0
a=rtpmap:111 opus/48000/2
a=ice-ufrag:abc123
a=ice-pwd:supersecret
a=fingerprint:sha-256 12:34:...
a=setup:actpass
a=mid:0
a=sendrecv
m=video 9 UDP/TLS/RTP/SAVPF 96 97
a=rtpmap:96 VP8/90000
a=rtpmap:97 H264/90000
a=sendrecv
Each m= line declares a media section (audio, video, datachannel). a= lines carry attributes: codecs (rtpmap), DTLS fingerprint, ICE credentials, simulcast layers. The answer keeps a subset of the offered codecs and finalizes the parameters both peers will use.
The application never reads or writes SDP fields directly; the browser's RTCPeerConnection produces and consumes it. But debugging WebRTC means reading SDP, and any SFU implementation must parse and rewrite SDP to insert its own ICE credentials and DTLS fingerprint.
Trickle ICE: shaving 200 ms off setup#
The original ICE spec required each peer to finish gathering every candidate before sending the SDP. With TURN allocations taking 50 to 200 ms each, that meant cold starts of 500 ms or more.
Trickle ICE (RFC 8838) sends the offer with whatever candidates are already known, then ships every new candidate as an asynchronous message as soon as it is discovered. The receiving peer adds candidates with addIceCandidate() and the connectivity checks start in parallel.
// Browser A: send candidates as they trickle in
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingSocket.send({
type: 'ice-candidate',
candidate: event.candidate.toJSON(),
});
}
};
// Browser B: ingest each candidate
signalingSocket.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.type === 'ice-candidate') {
pc.addIceCandidate(new RTCIceCandidate(data.candidate));
}
};
Without trickle, the median setup time is around 600 ms; with trickle, around 200 ms. Every major implementation uses it.
A working peer connection in JavaScript#
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com:3478',
username: 'demo',
credential: 'shared-secret',
},
],
iceTransportPolicy: 'all', // 'relay' forces TURN, useful for testing
};
const pc = new RTCPeerConnection(config);
// Add local media tracks
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: { width: 1280, height: 720 },
});
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
// Or a data channel
const channel = pc.createDataChannel('whiteboard', { ordered: false });
channel.onmessage = (e) => applyCursorPosition(JSON.parse(e.data));
// Wire ICE candidates to the signaling layer
pc.onicecandidate = ({ candidate }) => {
if (candidate) signaling.send({ type: 'ice', candidate });
};
// Caller path
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: offer.sdp });
// Callee path (mirror; on the other browser)
// await pc.setRemoteDescription({ type: 'offer', sdp });
// const answer = await pc.createAnswer();
// await pc.setLocalDescription(answer);
// signaling.send({ type: 'answer', sdp: answer.sdp });
// Show remote video
pc.ontrack = (event) => {
const [remoteStream] = event.streams;
document.getElementById('remoteVideo').srcObject = remoteStream;
};
// Inspect connection state
pc.oniceconnectionstatechange = () => {
console.log('ICE state:', pc.iceConnectionState);
// 'new' -> 'checking' -> 'connected' -> 'completed'
// or 'failed' / 'disconnected'
};
The iceServers array drives candidate gathering. With no TURN entry, the peer skips relay candidates entirely and the call fails on symmetric NATs.
DTLS-SRTP: encryption without a CA#
WebRTC does not use the TLS PKI. Instead, each peer generates an ephemeral DTLS certificate on the fly, exchanges the certificate fingerprint inside SDP (the a=fingerprint: line above), and runs a DTLS handshake over the same ICE-negotiated UDP socket. The handshake derives SRTP master keys via DTLS-SRTP key extraction (RFC 5764).
Two consequences:
- No third party can issue a fake cert: the only way to impersonate a peer is to compromise the signaling channel and inject a different fingerprint. This is why the signaling channel itself must run over HTTPS or WSS.
- Symmetric encryption is fast: SRTP uses AES-128 by default; modern phones encrypt 1080p60 video in single-digit-percent CPU.
The mandatory encryption is also why an SFU is a security choice, not just a scaling choice: the SFU terminates DTLS, sees decrypted RTP frames, and re-encrypts to each subscriber. End-to-end encrypted modes (Zoom E2EE, Meet E2EE) add a second layer of encryption on top, with keys never given to the SFU.
Mesh vs SFU vs MCU#
flowchart TB
subgraph Mesh
A1([A]) --- B1([B])
A1 --- C1([C])
B1 --- C1
end
subgraph SFU
A2([A]) --> SF[SFU]
B2([B]) --> SF
C2([C]) --> SF
SF --> A2
SF --> B2
SF --> C2
end
subgraph MCU
A3([A]) --> MX[MCU<br/>decode + composite + re-encode]
B3([B]) --> MX
C3([C]) --> MX
MX --> A3
MX --> B3
MX --> C3
end
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
class A1,B1,C1,A2,B2,C2,A3,B3,C3 client;
class SF service;
class MX compute;
| Topology | Per-client upload | Server cost | Scales to |
|---|---|---|---|
| Mesh (pure P2P) | N-1 streams | None | 4 to 5 peers |
| SFU | 1 stream | Forward only, no transcode | ~50 to 100 active video, hundreds with audio + thumbnails |
| MCU | 1 stream | Decode + composite + re-encode | Lower limit, very expensive per call |
Real products:
- Zoom uses an SFU per data center with proprietary congestion control. Its standout feature is simulcast layer switching: each peer uploads three layers (e.g. 1080p, 540p, 180p) and the SFU sends each subscriber the layer matching their downlink and viewport size.
- Google Meet uses an SFU with VP9 simulcast and dynamic layer selection driven by who is speaking.
- Discord voice channels use an SFU but for audio only on low-traffic channels (Opus, ~32 kbps per speaker, no simulcast complexity). Video and screen share kick in additional flows.
- Twilio Video, Daily, LiveKit, Mediasoup are SFU-as-a-service or self-hosted SFUs you build against.
MCUs (Multipoint Control Units) decode every input, composite into a single grid, and re-encode for each subscriber. The CPU cost is huge but the receiver only needs to decode one stream, which is why legacy hardware conferencing rooms used MCUs. Today MCUs survive only at the interop edge (linking SIP/H.323 endpoints into WebRTC calls).
Common failure modes#
| Symptom | Likely cause |
|---|---|
Stuck in iceConnectionState: checking forever |
TURN missing or wrong credentials; symmetric NAT on both sides |
| Works locally, fails over cellular | Mobile carrier-grade NAT; needs TURN |
| Audio works, video doesn't | Codec mismatch (e.g. one side only offers H264 hardware-only) |
| Random drops after 10 minutes | TURN session timeout, or UDP being aged out by NAT |
| Glitchy first second of a call | DTLS handshake competing with first RTP packets |
getUserMedia permission denied |
Page not served over HTTPS, or browser permission revoked |
chrome://webrtc-internals (or about:webrtc on Firefox) dumps the full ICE candidate list, the chosen pair, DTLS state, and per-track bitrates. This is the first place to look when a call fails.
Capacity numbers worth memorising#
- One STUN server pod handles roughly 50,000 requests per second on commodity hardware; one round trip per session at setup.
- One TURN pod handles 500 to 2,000 concurrent media sessions depending on bitrate and TLS overhead.
- An SFU pod (mediasoup, Janus, LiveKit) handles 500 to 2,500 concurrent participants per CPU core for audio-mostly; 200 to 800 for full video with simulcast.
- Setup latency: 200 ms with trickle ICE, 600 ms without.
- Bandwidth: 1080p video at 2 to 3 Mbps, 720p at 1 to 1.5 Mbps, audio Opus at 32 to 64 kbps.
FAQ#
How does WebRTC work?#
WebRTC sets up an encrypted peer-to-peer UDP channel between two browsers. Each side gathers ICE candidates (local IPs, STUN-discovered public IPs, TURN relay addresses), exchanges them with the other peer over a signaling channel, and the pair tries every combination until one connects. Media then flows directly over DTLS-SRTP.
Why does WebRTC need STUN and TURN servers?#
Most clients sit behind NAT, so their LAN IP is not reachable from the outside. STUN tells each peer its public IP and port as seen from the internet, which usually lets them connect directly. When symmetric NAT or strict firewalls block that, TURN relays the media through a server that both peers can reach.
Is the signaling channel part of WebRTC?#
No. WebRTC standardizes media transport and peer connection setup but deliberately leaves signaling to the application. Most products use WebSocket, HTTP long-poll, or SIP over WebSocket to ferry SDP offers, answers, and ICE candidates between peers.
When should you use an SFU instead of pure peer-to-peer WebRTC?#
Mesh peer-to-peer scales to roughly 4 to 5 participants because each peer encodes and uploads one stream per other peer. Beyond that, route through a Selective Forwarding Unit that receives one upload per peer and forwards to all subscribers. Zoom, Google Meet, and Discord voice all use SFUs.
What is the difference between a DataChannel and a MediaStream?#
MediaStream carries audio and video frames over SRTP with codec negotiation. DataChannel carries arbitrary application bytes over SCTP-over-DTLS with optional reliability and ordering. Both share the same ICE-negotiated UDP connection.
What is the difference between STUN and TURN?#
STUN is a stateless protocol that tells a client its public IP and port as seen from outside its NAT. The two peers usually use that information to send packets directly to each other. TURN is a stateful relay that forwards every media packet between the two peers when direct connectivity fails.
How does trickle ICE improve call setup time?#
Without trickle, each peer must finish gathering every candidate before sending the SDP, which can add hundreds of milliseconds because STUN and TURN allocations run sequentially. With trickle ICE, each candidate is shipped to the other peer as soon as it is discovered, so connectivity checks start in parallel with gathering.
Why does WebRTC use UDP instead of TCP?#
Real-time media tolerates a dropped frame far better than waiting for retransmission. TCP head-of-line blocking would freeze the entire stream while it recovers a single lost packet, producing audible glitches. SRTP over UDP lets the codec hide or skip losses without stalling the timeline.
What does an SFU do that pure peer-to-peer cannot?#
An SFU receives one upload per participant and forwards selected layers to each subscriber, so a 50-person meeting still asks each laptop to upload only one stream. It also enables simulcast layer switching, per-subscriber bitrate adaptation, recording, and lawful-intercept tap points.
Can WebRTC work without a TURN server?#
For roughly 80 to 90 percent of users behind common cone NATs, STUN alone is enough. The rest sit behind symmetric NAT, double NAT, or strict firewalls and will fail to connect without a TURN relay. Skipping TURN is a viable cost-saving for internal tools but unacceptable for consumer products.
What ports does WebRTC use?#
STUN and TURN typically run on UDP 3478 and TLS 5349. Media uses ephemeral UDP ports negotiated through ICE. In restrictive networks, TURN over TLS on port 443 is the most reliable fallback because it looks like HTTPS.
Which ICE candidate type is preferred?#
Host candidates have highest priority, then server-reflexive (STUN-discovered public address), then peer-reflexive, with relay (TURN) last. ICE pairs every local with every remote candidate and picks the highest-priority working pair.
How many peers can pure mesh WebRTC support?#
About 4 to 5 active video participants. Each peer encodes once but uploads one stream per other peer, so upload bandwidth and CPU both grow linearly. Anything larger should route through an SFU.
What percentage of WebRTC calls need TURN?#
Roughly 10 to 20 percent. The fraction climbs on mobile networks with carrier-grade NAT and corporate networks that block UDP entirely. Plan TURN capacity for at least 20 percent of expected calls.
Is WebRTC end-to-end encrypted?#
Between two peers, yes: DTLS-SRTP is mandatory and there is no plaintext mode. Through an SFU, the SFU sees decrypted media; true end-to-end encryption requires an extra frame-level encryption layer like Zoom E2EE or Meet E2EE.
Related Topics#
- WebSocket framing: the most common transport for the signaling channel that ferries SDP and ICE candidates
- UDP use cases: why WebRTC media uses UDP instead of TCP for real-time delivery
- Proxy types: a TURN server is functionally a media proxy with NAT-bypass semantics
- Zoom system design: the canonical large-scale WebRTC application
- Online whiteboard: DataChannel as the cursor and shape sync transport
Quick reference#
One-page cheat sheet for WebRTC, ICE, STUN, TURN, signaling, and SFU scaling. Use this to refresh the model before a system design interview.
The three helper servers#
| Server | Protocol | Stateful | Per-call cost | Optional |
|---|---|---|---|---|
| Signaling | App-defined (WebSocket, HTTP) | Briefly | Negligible (setup only) | No |
| STUN | UDP/3478 binding requests | No | Near zero | Often (LAN-only calls) |
| TURN | UDP/3478, TLS/5349 (or 443) | Yes (allocations) | Full media bandwidth | For ~80% of calls; mandatory at scale |
ICE candidate types#
| Type | Source | Priority |
|---|---|---|
host |
Local NIC IP | Highest |
srflx |
STUN-discovered public IP | High |
prflx |
Learned during connectivity check | Medium |
relay |
TURN allocation | Lowest |
ICE pairs local x remote candidates and tries the highest-priority working pair. First success wins.
NAT behaviors and what works#
| NAT type | Direct path | Needs TURN? |
|---|---|---|
| Full-cone | STUN works | No |
| Restricted-cone | STUN works | No |
| Port-restricted-cone | STUN works in most cases | Rarely |
| Symmetric | STUN fails | Yes |
| UDP blocked | Nothing UDP works | Yes (TCP/TLS TURN) |
Connection setup steps#
- Caller creates
RTCPeerConnection, callscreateOffer, sets local description, ships SDP through signaling. - Callee receives offer, sets remote description, calls
createAnswer, ships SDP back. - Both peers gather ICE candidates (host, srflx, relay) and trickle them through signaling.
- ICE runs STUN connectivity checks on every candidate pair.
- DTLS handshake on the chosen pair derives SRTP keys.
- Media (audio, video, DataChannel) flows directly over DTLS-SRTP.
Media channels#
| Channel | Wire protocol | Reliability | Use case |
|---|---|---|---|
MediaStream (audio) |
SRTP, Opus | Lossy | Voice |
MediaStream (video) |
SRTP, VP8/VP9/H264/AV1 | Lossy | Camera, screen share |
DataChannel |
SCTP over DTLS | Configurable (ordered/unreliable) | Game state, cursors, chat |
SFU sizing (rule of thumb)#
| Workload | Per CPU core |
|---|---|
| Audio-only (Opus 32 kbps) | 1,000 to 2,500 participants |
| 360p video with simulcast | 500 to 1,000 participants |
| 720p video with simulcast | 200 to 500 participants |
| 1080p video with simulcast | 100 to 250 participants |
Memory: roughly 1 to 5 MB per participant for jitter buffers and bookkeeping.
Bandwidth budget#
| Stream | Bitrate |
|---|---|
| Opus audio | 32 to 64 kbps |
| 180p video | 200 kbps |
| 360p video | 600 kbps |
| 720p video | 1.0 to 1.5 Mbps |
| 1080p video | 2.0 to 3.0 Mbps |
| Screen share (still slides) | 200 to 500 kbps |
| Screen share (motion) | 1.5 to 3 Mbps |
SDP cheat (what to know without reading specs)#
v=0: versionm=audio/m=video/m=application: media sectionsa=rtpmap:: codec definitiona=ice-ufrag:/a=ice-pwd:: ICE credentials for connectivity checksa=fingerprint:: DTLS certificate fingerprint (verifies the peer)a=setup:actpass: DTLS role negotiationa=sendrecv/a=sendonly/a=recvonly/a=inactive: directiona=simulcast:: simulcast layer description (used by SFUs)
Trickle ICE#
- Send the offer with whatever candidates you have so far.
- Ship each newly gathered candidate as it arrives via signaling.
- Receiver calls
addIceCandidate()for each. - Cuts median setup from ~600 ms to ~200 ms.
When to use what topology#
| Scenario | Topology |
|---|---|
| 1:1 call | Pure P2P with STUN, TURN fallback |
| 2 to 4 participants, internal tool | Mesh acceptable |
| 5 to 50 participants | SFU |
| 50 to 500 viewers, one broadcaster | SFU with cascading or live streaming (HLS, LL-HLS) |
| Legacy SIP/H.323 interop | MCU at the edge |
Real products#
| Product | Topology | Notes |
|---|---|---|
| Zoom | SFU per region | Simulcast layer switching, proprietary congestion control |
| Google Meet | SFU | VP9 simulcast, speaker-driven layer selection |
| Microsoft Teams | SFU | Largely WebRTC-compatible with custom client extensions |
| Discord voice | SFU (audio-focused) | Opus only on low-rate paths |
| Slack Huddles | SFU | Audio + light screen share |
| Twilio Video, Daily, LiveKit | SFU-as-a-service | Build-on platforms |
Common failure modes#
| Symptom | Cause |
|---|---|
ICE stuck on checking |
TURN missing or both peers on symmetric NAT |
| Works on Wi-Fi, fails on LTE | Carrier-grade NAT; needs TURN |
| Video freezes; audio fine | Codec mismatch or video bitrate exceeds path |
| Drops after 5 to 10 minutes | NAT pinhole aged out; missing keepalives |
| First second glitches | DTLS handshake competing with media |
getUserMedia permission denied |
Page not HTTPS, or browser permission revoked |
Debug starts at chrome://webrtc-internals for the chosen ICE pair, DTLS state, and per-track stats.
Ops checklist#
- Run STUN as anycast for low RTT.
- Run TURN regionally; latency penalty for relay can double end-to-end RTT.
- Always offer TURN-over-TLS on 443 for restrictive corporate networks.
- Rotate TURN credentials (short-lived HMAC tokens) so URLs in client code do not become open relays.
- Monitor
relayratio: if it climbs above 25 percent, investigate NAT regression or TURN region health. - Cap simulcast layers per participant (3 is the universal answer) to keep upload bandwidth predictable.
Security one-liner#
DTLS-SRTP is mandatory; the signaling channel must be HTTPS/WSS so attackers cannot inject a fake DTLS fingerprint. Through an SFU, end-to-end encryption requires an extra frame-encryption layer.