What is WebRTC?
WebRTC (Web Real-Time Communication) is an open-source technology that enables peer-to-peer audio, video, and data sharing directly between browsers and mobile apps without browser plugins. Signaling and sometimes relay or media servers are still required.
The connection choice: Media may flow directly between peers, through a TURN relay or through a media server such as an SFU.
Why P2P? The Latency Problem
Traditional streaming (Client → Server → Client): You → YouTube Server → Friend Latency: 5-30 seconds (buffering, processing) WebRTC (Peer-to-Peer): You → Friend (direct connection) Latency: 50-300ms (near real-time) Use cases: Video calls, live gaming, screen sharing
Comparison
| Feature | Traditional Streaming | WebRTC P2P |
|---|---|---|
| Latency | Depends on buffering and protocol | Depends on network, codec and playout configuration |
| Server Cost | Depends on delivery topology | Signaling plus any TURN/SFU traffic |
| Quality | Buffered, stable | Real-time, adaptive |
| Use Case | YouTube, Netflix | Zoom, Discord, Gaming |
Core Components
WebRTC has 3 main APIs:
1. MediaStream (getUserMedia)
Access camera and microphone.
// Get video and audio from user's device
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
audio: true
});
// Display on page
const videoElement = document.querySelector('video');
videoElement.srcObject = stream;
2. RTCPeerConnection
Create peer-to-peer connection.
// Create connection
const peerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
// Add local stream
stream.getTracks().forEach(track => {
peerConnection.addTrack(track, stream);
});
// Receive remote stream
peerConnection.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0];
};
3. RTCDataChannel
Send arbitrary data peer-to-peer.
// Create data channel
const dataChannel = peerConnection.createDataChannel('chat');
dataChannel.onopen = () => {
dataChannel.send('Hello!');
};
dataChannel.onmessage = (event) => {
console.log('Received:', event.data);
};
The NAT Problem
Problem: Most devices don't have public IP addresses. They're behind routers with NAT (Network Address Translation).
Your Computer: 192.168.1.5 (private IP)
↓
Router NAT
↓
ISP: 203.0.113.42 (public IP)
Challenge: Friend can't connect to 192.168.1.5 directly. They need your public IP and port.
NAT Traversal: STUN, TURN, ICE
STUN Server (Session Traversal Utilities for NAT)
"Mirror" server that tells you your public IP and port.
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
const pc = new RTCPeerConnection(config);
How it works:
You → STUN Server: "What's my public address?" STUN → You: "You are 203.0.113.42:54321" You → Friend: "Connect to me at 203.0.113.42:54321"
TURN Server (Traversal Using Relays around NAT)
Relay server when direct P2P fails (e.g., strict corporate firewalls).
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'password'
}
]
};
When needed:
Symmetric NAT or Strict Firewall:
You ← Can't connect directly → Friend
↓ ↓
TURN Server (relay)
You → TURN → Friend
(Slower, but works)
Cost: TURN servers relay all media traffic = expensive bandwidth
ICE (Interactive Connectivity Establishment)
Protocol that tries multiple connection methods, picking the best.
ICE candidates (in order of preference): 1. Host candidate: Direct LAN connection (192.168.x.x) 2. Server reflexive: STUN-discovered public IP (P2P via router) 3. Relay candidate: TURN server relay (fallback) ICE tries all candidates, uses fastest successful connection
WebRTC Connection Flow
Step-by-Step
- Get media: Both users grant camera/mic access
- Create offer: Alice creates SDP offer describing her capabilities
- Signal offer: Alice sends offer to Bob via signaling server (WebSocket)
- Create answer: Bob creates SDP answer
- Signal answer: Bob sends answer to Alice
- ICE candidates: Both exchange network information
- Connect: WebRTC establishes best possible connection
- Stream: Media follows the selected direct or relayed path
Two-peer signaling sketch
This local sketch assumes trusted test peers and a controlled room. It is not a deployable conferencing service: add authenticated room membership, signaling authorization, input limits, HTTPS, TURN credentials, glare handling, reconnection and media cleanup. A real server must not forward arbitrary signaling to an unchecked target socket.
Signaling Server (Node.js + Socket.IO)
// server.js
const express = require('express');
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);
const rooms = new Map();
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join-room', (roomId) => {
socket.join(roomId);
// Notify others in room
socket.to(roomId).emit('user-joined', socket.id);
});
// Forward WebRTC signaling messages
socket.on('offer', (data) => {
socket.to(data.target).emit('offer', {
offer: data.offer,
sender: socket.id
});
});
socket.on('answer', (data) => {
socket.to(data.target).emit('answer', {
answer: data.answer,
sender: socket.id
});
});
socket.on('ice-candidate', (data) => {
socket.to(data.target).emit('ice-candidate', {
candidate: data.candidate,
sender: socket.id
});
});
socket.on('disconnect', () => {
socket.broadcast.emit('user-left', socket.id);
});
});
server.listen(3000, '127.0.0.1');
Client (JavaScript)
// client.js
const socket = io('http://localhost:3000');
const roomId = 'room123';
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
let localStream;
let peerConnections = {};
const pendingCandidates = new Map();
async function flushCandidates(peerId, pc) {
for (const candidate of pendingCandidates.get(peerId) || []) {
await pc.addIceCandidate(candidate);
}
pendingCandidates.delete(peerId);
}
// Get local media
async function startCall() {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
document.getElementById('localVideo').srcObject = localStream;
socket.emit('join-room', roomId);
}
// Create peer connection
function createPeerConnection(peerId) {
const pc = new RTCPeerConnection(config);
// Add local stream
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
// Handle remote stream
pc.ontrack = (event) => {
const remoteVideo = document.getElementById(`remote-${peerId}`);
if (remoteVideo) remoteVideo.srcObject = event.streams[0];
};
// Handle ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('ice-candidate', {
target: peerId,
candidate: event.candidate
});
}
};
peerConnections[peerId] = pc;
return pc;
}
// User joined room
socket.on('user-joined', async (peerId) => {
const pc = createPeerConnection(peerId);
// Create and send offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', {
target: peerId,
offer: offer
});
});
// Receive offer
socket.on('offer', async ({ offer, sender }) => {
const pc = createPeerConnection(sender);
await pc.setRemoteDescription(new RTCSessionDescription(offer));
await flushCandidates(sender, pc);
// Create and send answer
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', {
target: sender,
answer: answer
});
});
// Receive answer
socket.on('answer', async ({ answer, sender }) => {
const pc = peerConnections[sender];
await pc.setRemoteDescription(new RTCSessionDescription(answer));
await flushCandidates(sender, pc);
});
// Receive ICE candidate
socket.on('ice-candidate', async ({ candidate, sender }) => {
const pc = peerConnections[sender];
if (pc?.remoteDescription) {
await pc.addIceCandidate(candidate);
} else {
const pending = pendingCandidates.get(sender) || [];
pending.push(candidate);
pendingCandidates.set(sender, pending);
}
});
// User left
socket.on('user-left', (peerId) => {
if (peerConnections[peerId]) {
peerConnections[peerId].close();
delete peerConnections[peerId];
pendingCandidates.delete(peerId);
}
});
Protocols & Codecs
Transport protocols
Media normally uses secure RTP over an ICE-selected transport. TURN can relay when a direct path is unavailable. Data channels use SCTP over DTLS and can be configured for ordered/unordered and reliable/partially reliable delivery. A reliable data channel is not simply a fallback to TCP. W3C WebRTC specification.
Media Protocols
RTP (Real-time Transport Protocol): - Delivers audio/video packets - Timestamps and sequence numbers - No encryption SRTP (Secure RTP): - Encrypted version of RTP - Used by WebRTC - Prevents eavesdropping RTCP (RTP Control Protocol): - Quality metrics and feedback - Packet loss, jitter, round-trip time
Video codecs
Query the actual browser’s capabilities and negotiate a mutually supported codec. Codec efficiency, hardware acceleration and device load depend on implementation; a fixed ranking is not a deployment plan.
const capabilities = RTCRtpReceiver.getCapabilities('video');
for (const codec of capabilities?.codecs ?? []) console.log(codec.mimeType);
Application patterns
1. Multiparty conferencing
An SFU forwards selected encoded streams. An MCU decodes and mixes media before sending a combined output. Calling SFU forwarding “mixing” hides the CPU and client-layout tradeoffs.
2. Voice chat
Capture audio with getUserMedia and add its audio tracks to a peer connection. Creating a data channel named voice does not capture, encode or play audio by itself.
3. Screen Sharing
// Capture screen instead of camera
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: 'always' },
audio: false
});
peerConnection.addTrack(screenStream.getVideoTracks()[0]);
4. File transfer
A data channel can send file chunks, but the sender must respect bufferedAmount and resume using bufferedamountlow; repeatedly calling send() can otherwise accumulate a large memory queue. Negotiate chunk size, file metadata, integrity verification, completion and cancellation. Stop sending when the channel closes. These are application responsibilities even when transport delivery is reliable.
Scaling Challenges
Mesh (Full P2P)
3 participants: 3 connections A ←→ B A ←→ C B ←→ C 10 participants: 45 connections! Each user uploads to 9 others = 9x bandwidth
Limitation: Each participant’s upload and encoding work grows with the others. The practical limit depends on bitrate, device and network, not a fixed participant count.
SFU (Selective Forwarding Unit)
Server forwards streams without mixing A → SFU → B, C, D B → SFU → A, C, D C → SFU → A, B, D Each client uploads 1 stream, downloads N-1 streams Server load: Linear with participants
An SFU is a common multiparty topology; specific products may use different architectures across clients and call modes.
MCU (Multipoint Control Unit)
Server mixes all streams into one A, B, C → MCU (mixes) → Composed stream → A, B, C Each client uploads 1, downloads 1 Server load: High (CPU encoding)
Interview Tips
When discussing WebRTC in system design interviews:
- Explain P2P benefit: "Compare direct and relayed paths using measured latency and loss; a relay is not inherently a multi-second buffer..."
- NAT traversal: "We'd use STUN for most users, TURN relay as fallback for strict firewalls..."
- Signaling server: "Need WebSocket server for SDP offer/answer exchange, not for media..."
- Scaling: "As participant upload grows, evaluate an SFU that forwards streams without necessarily mixing them..."
- Fallback: "Test TURN over permitted transports; any broadcast fallback changes latency and interaction semantics..."
- Real examples: "Distinguish browser WebRTC APIs from any one product’s implementation..."
Worked failure: signaling succeeds but media never arrives
A successful offer/answer exchange proves that signaling worked, not that peers found a usable network path. Inspect ICE connection state and the selected candidate pair. Test from a restricted network with a working TURN configuration. Queue candidates received before the remote description, and distinguish permission denial from connectivity failure. Then inspect bytes received and decoded frames before blaming the camera.
For a four-person call, budget each sender’s upload before choosing mesh. If every sender transmits three independent 1 Mbps streams, its media upload is roughly 3 Mbps before overhead. An SFU can reduce sender duplication while increasing server egress; that is a topology budget, not a guaranteed benchmark.
Related Concepts
- WebSockets — Used for WebRTC signaling
- TCP — Transport protocol trade-offs
- CDN — Deliver TURN servers from edge locations
- Load Balancing — Distribute SFU server load
- Video transcoding — Codec selection and optimization
Keep reading
DNS Architecture
The phonebook of the internet. How Domain Name System works, the DNS hierarchy, and recursive vs iterative resolution strategies.
HTTP Evolution (H1 to H3)
From text-based HTTP/1.1 to binary HTTP/2 and UDP-based HTTP/3 (QUIC). Why we needed upgrades and how they solve Head-of-Line Blocking.
TCP Handshake & Congestion
The 3-way handshake that powers the internet. SYN, SYN-ACK, ACK. Flow control vs Congestion control, and modern algorithms like BBR.
WebSockets vs Polling
Comprehensive comparison of real-time communication techniques including short polling, long polling, Server-Sent Events (SSE), and WebSockets for building responsive web applications.
BitTorrent: Pieces, Peer Discovery and Swarm Limits
Understand verified piece exchange, v1 versus v2 metadata, peer discovery, and the capacity limits of cooperative distribution.
Content Delivery Networks: Cache Keys, Freshness and Origin Load
Choose safe cache policies, calculate origin offload, and test personalized responses, invalidation and cold-cache recovery.