WebRTC Deep Dive: From Peer to Peer to Global Scale
Every time you hop on a Zoom call, drop into a Discord voice channel, or share your screen on Google Meet, one protocol is doing the heavy lifting. That protocol is WebRTC, and it pulls off something most engineers never have to think about: voice and video latency of under 200 milliseconds, across the planet, inside a browser tab, with no install.
That number is worth sitting with. Under 200 milliseconds is faster than a typical TCP handshake. It is faster than the JSON request your app probably just fired to load this page. Most of us live in a world of HTTP and WebSockets, where a few hundred milliseconds is fine. WebRTC lives in a different world, where a few hundred milliseconds means a frozen face and robotic audio.
This is the full breakdown of how it works, why each piece exists, and where it gets painful in production.
From Plugins to a Browser Standard#
Before Google open sourced WebRTC in 2011, doing a video call in a browser meant Flash, a Java applet, or some custom plugin. These were insecure, painful to maintain, and behaved differently in every browser. If you ever clicked "install this plugin to join the call," you lived through that era.
By 2017 WebRTC was built into every major browser. Chrome, Firefox, Safari, and Edge all shipped it natively. The browser quietly became a real time communication endpoint. No downloads, no plugins, just an API sitting there waiting to be used.
The Mental Model: Transactions vs Sessions#
HTTP is a transaction. The client asks, the server answers, and the conversation is over. It is stateless and predictable. You already know this model in your bones.
WebRTC is a session. Before a single frame of video moves, two peers have to negotiate. They hold a back and forth conversation about who they are, how they can be reached, and what they are capable of. They trade descriptions of their media and network using a document called SDP, the Session Description Protocol. Once both sides agree, a direct pipe opens and stays open for the life of the call.
That shift, from transaction to session, is the root of almost everything that makes WebRTC feel different.
UDP: Why Late Data Is Dead Data#
Here is the decision that defines WebRTC: it runs on UDP, not TCP.
TCP guarantees that every packet arrives, in order, exactly once. To keep that promise, it stops and waits whenever a packet goes missing, asking for a retransmit. That wait can cost 100 to 200 milliseconds or more. For a file download, that is the right tradeoff. For a live call, it is a disaster, because the entire stream freezes while TCP waits for one missing piece.
UDP is unreliable on purpose. If a packet is lost, it is gone forever, and the stream keeps moving. In real time, a packet that shows up 300 milliseconds late is worse than a packet that never arrives at all. Dropping one frame of video or skipping a tiny syllable is invisible. Freezing the whole call for half a second is not.
Toggle between the two below and watch what happens when packets 4 and 7 go missing.
Late data is dead data · UDP
TCP optimises for correctness. UDP optimises for time. WebRTC chooses time, every single time.
One API, Seven Protocols#
WebRTC looks like one thing from the outside, but under the hood it is a bundle of seven protocols working together. The browser hides the complexity behind a clean API, but it helps to know what is actually down there. Tap through each layer.
One API, seven protocols · tap a layer
What can each side do?
A plain text document the two peers swap before any media flows. It lists supported codecs, resolutions, and how the browser can be reached. Both sides must agree before a pipe opens.
These seven pieces are standardised by the IETF and the W3C, which is exactly why the same WebRTC code behaves identically across Chrome, Firefox, Safari, and Edge. You write once, and the plumbing is consistent everywhere.
The NAT Problem and Signaling#
Here is the awkward truth about your laptop: it does not have a public address. It sits behind NAT, Network Address Translation, on a private home network with an address like 192.168.1.42. To the rest of the internet, your laptop is invisible. A router only lets in responses to requests that went out. So how does a peer on the far side of the world send video to a device hiding behind your router?
WebRTC solves this with three moving parts:
- A signaling server. This is the matchmaker, and you have to build it yourself, usually with WebSockets. Its only job is to introduce the two peers by passing SDP offers, answers, and address candidates between them. It is not part of the WebRTC standard, but you cannot start a call without one.
- ICE candidates. Each peer gathers a list of every possible address where it might be reachable: its local private address, its public address discovered through STUN, and a relay address from TURN.
- The ICE process. The peers swap these candidate lists through the signaling server and then fire connection attempts at every possible pair until they find the fastest path that actually works.
Most of the time STUN is enough and the peers connect directly. But roughly 10 to 15 percent of the time the network is hostile, and the call has to fall back to a TURN relay. Flip the network condition below to see both outcomes.
Getting through NAT · STUN direct
The key distinction: STUN is cheap and only used briefly to discover an address. TURN is expensive because it carries the full media stream for the entire call. You want STUN. You pay for TURN.
The Browser API in Practice#
For all that machinery underneath, the code you write is small. In about twenty lines you create a connection, attach your camera and microphone, make an offer, and point the incoming stream at a video element.
// 1. Create the connection, pointing it at a STUN server
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
})
// 2. Add your local camera and mic tracks
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
stream.getTracks().forEach((track) => pc.addTrack(track, stream))
// 3. When the browser finds an address candidate, send it to the other peer
pc.onicecandidate = ({ candidate }) => {
if (candidate) signaling.send({ candidate })
}
// 4. When a remote track arrives, show it
pc.ontrack = ({ streams }) => {
remoteVideo.srcObject = streams[0]
}
// 5. Create the SDP offer and kick off the handshake
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
signaling.send({ sdp: offer })The other side runs the mirror image: it calls setRemoteDescription with your offer, creates an answer, and sends it back. Once the ICE candidates have been exchanged and a path is chosen, media starts flowing and the signaling server steps out of the way.
Beyond Video: The Data Channel#
WebRTC is not only about audio and video. It also gives you RTCDataChannel, a pipe for sending arbitrary low latency binary data directly between peers.
This is the quiet workhorse behind a lot of products you use. It syncs cursor positions in Figma, carries real time game state in multiplayer games, and powers peer to peer file transfers. Anywhere you need data to move fast and directly, the data channel is the tool.
The Scaling Problem: P2P vs SFU#
Pure peer to peer is beautiful for two people. Each side sends one stream and receives one stream. Clean.
Now add people. In a pure mesh, every person has to upload their video to every other person. For ten people, each person uploads nine separate video streams at the same time. No home connection survives that. The cost grows quadratically, and the call collapses.
Drag the slider and watch a home connection get crushed, then switch to an SFU and see what changes.
Why P2P breaks · P2P
The fix is the SFU, the Selective Forwarding Unit. It is a smart router that sits in the middle of the call. Each person uploads their video exactly once to the SFU, and the SFU forwards it to everyone else. Uploads per person drop to one, no matter how big the room gets. This is linear scaling, and it is what lets Discord pack thousands of people into a stage channel or Zoom hold a hundred people in one meeting.
The clever trick layered on top is simulcast. The client sends two or three quality versions of its video at once, say one in HD and one in low resolution. The SFU then picks the right version for each viewer based on their screen size and connection. Someone on a tiny phone on bad wifi gets the low res feed. Someone on a big monitor gets HD. If one viewer's connection drops, only that person is downgraded, and everyone else keeps their quality.
SFU vs MCU: Why Nobody Mixes Anymore#
There is an older architecture called the MCU, the Multipoint Control Unit. Instead of forwarding packets, it decodes every incoming stream, mixes them into a single composite video, encodes that again, and ships one combined feed to each participant. Think of the old "Brady Bunch" grid baked into one stream.
The difference matters enormously for cost.
SFU vs MCU · why nobody mixes anymore
Because the MCU does heavy video processing, it burns roughly ten times the CPU of an SFU. By 2026 almost no modern product uses an MCU. They survive mostly in legacy corporate phone systems. The SFU won because it forwards without decoding, which keeps it fast, cheap, and able to make smart per viewer decisions.
Case Study: How Discord Does It#
Discord is a great real world example. They run regional voice servers, which are SFUs placed close to users to keep round trip times short. The closer the server, the snappier the call.
Their stack splits the work by strength. They use Elixir for the signaling layer, managing who is in which channel, because it handles huge numbers of lightweight concurrent connections gracefully. They use C++ for the media routing, where raw performance matters. And they lean on the Opus codec, which can scale from a thin 6 kilobits per second when bandwidth is tight all the way up to studio quality when the connection is healthy.
Here is the irony worth remembering: even though WebRTC is sold as "peer to peer," in production your voice almost always passes through a server like Discord's. Pure peer to peer is the exception, not the rule, once you go past two people.
Production Gotchas Nobody Warns You About#
Building a toy WebRTC demo is a weekend. Shipping a real product surfaces a pile of unglamorous problems.
- TURN servers cost real money. Relaying full media bandwidth for the 10 to 15 percent of users stuck behind strict firewalls is expensive, and you have to run and maintain the relays yourself, often with a project like Coturn.
- Mobile networks fight you. Some carriers throttle or block UDP outright. If you do not implement a TCP or TLS fallback, those users silently fail to connect.
- Recording is its own project. Since an SFU never decodes the video, you cannot just "save the call." You need a separate recorder component that joins the call as a participant, receives the streams, and assembles the recording.
None of these show up in the tutorial. All of them show up in production.
Wrapping Up#
WebRTC is a stack of careful tradeoffs. It picks UDP over TCP because late data is dead data. It bundles seven protocols behind one API so the browser can hide the mess. It uses STUN to find direct paths and TURN to rescue the calls that cannot. And it scales past two people by quietly routing everything through an SFU, even though it wears a peer to peer label.
The next time a call connects in under 200 milliseconds, you will know exactly how much machinery just got out of your way.
Related posts
Sponsor
Support my open-source work
If my projects, blog posts, or tools have helped you, consider sponsoring me on GitHub. Every contribution keeps the side projects shipping.