Skip to content
Back to all concepts
NetworkingProtocolsReal-time

WebRTC (Peer-to-Peer Communication)

Browser APIs and protocols for real-time audio, video and data, with signaling, ICE connectivity checks, TURN relay fallback and multiparty media-server tradeoffs.

By ScaleWikiUpdated 5 min readAdvanced

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

FeatureTraditional StreamingWebRTC P2P
LatencyDepends on buffering and protocolDepends on network, codec and playout configuration
Server CostDepends on delivery topologySignaling plus any TURN/SFU traffic
QualityBuffered, stableReal-time, adaptive
Use CaseYouTube, NetflixZoom, Discord, Gaming

Core Components

WebRTC has 3 main APIs:

1. MediaStream (getUserMedia)

Access camera and microphone.

javascript
// 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.

javascript
// 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.

javascript
// 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.

javascript
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).

javascript
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

  1. Get media: Both users grant camera/mic access
  2. Create offer: Alice creates SDP offer describing her capabilities
  3. Signal offer: Alice sends offer to Bob via signaling server (WebSocket)
  4. Create answer: Bob creates SDP answer
  5. Signal answer: Bob sends answer to Alice
  6. ICE candidates: Both exchange network information
  7. Connect: WebRTC establishes best possible connection
  8. 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)

javascript
// 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)

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.

javascript
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

javascript
// 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:

  1. Explain P2P benefit: "Compare direct and relayed paths using measured latency and loss; a relay is not inherently a multi-second buffer..."
  2. NAT traversal: "We'd use STUN for most users, TURN relay as fallback for strict firewalls..."
  3. Signaling server: "Need WebSocket server for SDP offer/answer exchange, not for media..."
  4. Scaling: "As participant upload grows, evaluate an SFU that forwards streams without necessarily mixing them..."
  5. Fallback: "Test TURN over permitted transports; any broadcast fallback changes latency and interaction semantics..."
  6. 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.