Real-time features all look the same from the outside (a chat that updates instantly, a video call, a live cursor) but under the hood they split into two very different mechanisms: relayed (WebSockets) and peer-to-peer (WebRTC).
WebSockets: a persistent, two-way pipe
A normal HTTP request is one-shot: ask, get an answer, connection closes. A WebSocket upgrades that into a single long-lived TCP connection both sides can push through at any time: no polling, no re-handshaking per message. Socket.IO wraps this in rooms, automatic reconnection, and a fallback transport, which is why it's the default choice for chat, notifications, and live dashboards.
The catch: every message still round-trips through your server. Fine for small JSON events. Not fine for continuous audio/video: that's a lot of bandwidth to relay for no reason when the two participants could just talk to each other directly.
WebRTC: browser-native peer-to-peer
WebRTC lets two browsers exchange audio, video, and arbitrary data directly, with no server in the media path. It has three moving parts:
1. Signaling
WebRTC deliberately doesn't define how two peers find each other: that's left to you, and in practice it's almost always your existing WebSocket/Socket.IO server. Its only job is ferrying two messages back and forth: an SDP offer from the caller and an SDP answer from the callee, describing codecs and media capabilities each side supports.
2. NAT traversal: STUN and TURN
Most devices sit behind a router doing NAT, so they don't know their own public address. A STUN server answers "what does the internet see me as?", which is enough for a direct connection most of the time. When both sides are behind strict/symmetric NAT (common on corporate networks), no direct path exists, and a TURN server relays the media instead, slower, but guaranteed to work.
3. Media & data channels
navigator.mediaDevices.getUserMedia() grabs camera/mic as a MediaStream. Its tracks get added to an RTCPeerConnection, and the matching tracks show up in a <video> element on the other end once the connection is up. Alongside that, RTCDataChannel opens a second peer-to-peer pipe for plain data (game state, file chunks, cursor positions) with the same low latency as the media path.
Wiring up a minimal video call
- Both browsers connect to a signaling server (a Socket.IO namespace works fine).
- The caller creates an
RTCPeerConnection, attaches its local stream, and sends an offer through signaling. - The callee creates its own connection, sets the offer as remote description, and sends back an answer.
- Both sides trickle ICE candidates to each other as they're discovered.
- Once ICE finishes negotiating a path (direct or via TURN), media starts flowing directly between the two browsers. The signaling server is no longer involved.
Where peer-to-peer stops scaling
A mesh topology (every participant connects to every other participant) works for a 1:1 call and holds up reasonably to 4 to 6 people. Past that, each participant's upload bandwidth and CPU cost multiply per additional peer, and it falls over. That's the point where you'd introduce an SFU (Selective Forwarding Unit): each peer uploads its stream once, and the server fans it out to everyone else, trading a bit of latency for a topology that actually scales.
Draft placeholder. Written as a sample while wiring up the blog. Replace with your own version.