What is WebRTC and How Does It Work?

WebRTC is the set of APIs and protocols that lets a browser send audio, video, and arbitrary data to another browser without a plugin. That much every introduction tells you. What most of them skip is the part that actually determines whether your application works: WebRTC is not one technology but a stack of about a dozen protocols, and the layer that fails in production is almost never the one you spent your time on.

This article walks the stack in the order a real connection walks it — capture, negotiate, connect, transmit — and is honest about which parts the browser hands you for free and which parts are still your problem.

The three APIs, and the one that causes all the trouble

Everything the browser exposes lives behind three objects. Two of them are straightforward and one of them is where you will spend your debugging life.

getUserMedia() asks the operating system for a camera and microphone and hands back a MediaStream. It is a permissions API more than a media API: it fails on insecure origins, it fails when another application holds the device, and on iOS Safari it historically fails when called outside a user gesture. It does not touch the network.

const stream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: { width: { ideal: 1280 }, height: { ideal: 720 } },
});

RTCDataChannel gives you an ordered-or-unordered, reliable-or-unreliable message stream over SCTP. It is the piece people reach for when they want "WebSockets but peer-to-peer," and it behaves well once the connection exists.

RTCPeerConnection is the other 90%. It owns codec negotiation, encryption, congestion control, and — critically — connectivity. When a WebRTC application is broken, the fault is inside this object roughly every time, and usually in its connectivity half rather than its media half.

How a connection actually gets made

WebRTC connection at a glance Two browsers use a signaling server only to exchange SDP offers/answers and ICE candidates during setup. Once connected, encrypted media and data flow directly between the browsers, peer to peer. Signaling server setup only Browser A Browser B SDP offer/answer + ICE SDP offer/answer + ICE direct peer-to-peer encrypted media & data · DTLS / SRTP
Figure 1 — The signaling server only helps peers find each other. Once ICE picks a path, media and data travel between the browsers directly, already encrypted.

Four things happen, and they overlap more than diagrams suggest.

1. The offer/answer exchange (and the transport you have to build yourself)

The caller creates an SDP offer describing what it wants to send and what codecs it supports; the callee replies with an answer. WebRTC generates and parses these blobs for you but deliberately does not ship them anywhere. Getting an offer from A to B is your job, over whatever transport you like — a WebSocket, an HTTP POST, a Firebase document, an SSE stream.

This is the single most surprising thing about WebRTC for newcomers: the spec has no signaling protocol. The reasoning is that real applications already have a session concept, an auth system, and a presence model, and forcing a second one on them would be worse than leaving it open. The practical consequence is that "WebRTC" is never a drop-in — there is always a server you have to write. We cover the shape of that server in WebRTC signaling explained.

2. ICE gathering — finding paths that might work

In parallel with the SDP exchange, each peer collects candidates: addresses at which it might be reachable. There are three kinds, and knowing them is what turns a mysterious failure into a five-minute fix.

  • host — a local interface address. Free, no server involved, and usually useless across the internet. Modern browsers hide these behind randomized .local mDNS names so a web page cannot fingerprint your LAN.
  • srflx (server-reflexive) — your public IP and port as observed by a STUN server. This is what lets two peers behind ordinary home routers connect directly.
  • relay — an address on a TURN server that will forward packets on your behalf. Costs money, always works, and is the fallback when nothing else does.

Candidates are produced asynchronously and sent to the far side as they appear — "trickle ICE" — rather than waiting for gathering to finish. That is why your signaling channel must stay open after the offer and answer are exchanged.

3. Connectivity checks and pair selection

Both peers now pair every local candidate with every remote candidate and probe each pair with STUN binding requests. Pairs that answer become valid; ICE nominates one, preferring higher-priority pairs, which in practice means direct paths over relayed ones. ICE explained covers the priority arithmetic.

Nothing in this phase reports a friendly error. If no pair succeeds, connectionState simply becomes failed after a timeout, with no indication of why. That opacity is the reason tooling matters here.

4. DTLS handshake, then media

Once a pair is nominated, the peers run a DTLS handshake over it and derive SRTP keys from the result. Only then does media flow. A connection that reaches connected and then dies seconds later is often a DTLS problem, not an ICE one — a distinction worth making early, since the fixes are completely different.

The whole handshake, in one screen of code

Descriptions of the offer/answer dance are easy to nod along to and hard to reconstruct. Here is the caller side end to end, with the parts people forget marked. signal() and onSignal() stand in for whatever transport you chose; everything else is real.

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    // A TURN entry is not optional in production — see the relay-ratio discussion below.
    { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'pass' },
  ],
});

// 1. Local media must be attached BEFORE createOffer(), or the offer
//    describes a receive-only session and you will wonder why nothing sends.
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
stream.getTracks().forEach((track) => pc.addTrack(track, stream));

// 2. Trickle ICE: candidates arrive over time, long after the offer is sent.
//    Keep the signaling channel open or you will silently lose the relay candidate,
//    which is usually the only one that would have worked.
pc.onicecandidate = ({ candidate }) => {
  if (candidate) signal({ type: 'candidate', candidate });
};

// 3. Watch the state machine rather than assuming success.
pc.onconnectionstatechange = () => {
  console.log('connection:', pc.connectionState); // new → connecting → connected | failed
};

// 4. Remote media arrives here, not as a return value from anything.
pc.ontrack = ({ streams }) => {
  document.querySelector('#remote').srcObject = streams[0];
};

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signal({ type: 'offer', sdp: pc.localDescription });

onSignal(async (msg) => {
  if (msg.type === 'answer') {
    await pc.setRemoteDescription(msg.sdp);
  } else if (msg.type === 'candidate') {
    // addIceCandidate can legitimately reject for candidates that arrive
    // before the remote description; swallowing it here is normal.
    try { await pc.addIceCandidate(msg.candidate); } catch {}
  }
});

The callee is the same shape with setRemoteDescription(offer) before createAnswer(). Two details in that listing account for a large share of "my code follows the tutorial and still fails":

  • Tracks added after createOffer() produce an SDP that never advertises them. Renegotiation fixes it, but the first call looks broken.
  • Closing the signaling socket once the answer arrives drops the candidates that trickle in afterward. On a good network you never notice, because the host or srflx pair already succeeded. On the restrictive network where only the relay candidate would have worked, the call fails — and it fails only for the users you cannot reproduce.

"Peer-to-peer" is a default, not a guarantee

WebRTC is routinely described as a peer-to-peer technology, and the marketing conclusion drawn from that — no servers, no bandwidth bill — is wrong often enough to be worth correcting.

Two things break the picture. First, some fraction of connections cannot go direct at all: symmetric NAT, strict corporate firewalls, and carrier-grade NAT on mobile networks all defeat STUN, and those calls fall back to a TURN relay that you pay for by the gigabyte. Commonly cited figures put this at roughly 10–20% of connections for a general consumer audience — a rule of thumb rather than a measurement, and one you should replace with your own number, because an enterprise or mobile-heavy audience runs much higher. Measuring your relay ratio shows how.

Second, mesh peer-to-peer stops scaling almost immediately. Every participant sends a separate encoded stream to every other participant, so upstream bandwidth grows linearly with room size while encoder load grows with it. Past four or five people, essentially every production application routes media through an SFU — a server that receives one stream per sender and forwards it — which means the connection is peer-to-server-to-peer. See SFU vs MCU vs mesh.

So: WebRTC lets you avoid a media server for small sessions on friendly networks. That is genuinely valuable. It is not the same as running without infrastructure.

Encryption is mandatory, and it has consequences

There is no unencrypted WebRTC. Media is protected by SRTP with keys negotiated over DTLS, data channels ride SCTP over DTLS, and the browser will not let you turn any of it off. The getUserMedia() and RTCPeerConnection APIs are also gated on secure contexts, so the page itself has to be HTTPS or localhost.

Two consequences follow that people learn the hard way:

  • A TURN relay cannot read your media. It forwards SRTP packets whose keys it never had. This is why relaying through a third-party TURN provider is a bandwidth and metadata decision rather than a content-confidentiality one.
  • Transport-level encryption is not application-level authentication. DTLS proves the two endpoints agreed on keys; it does not prove the person on the other end is who your product says they are. Identity is signaling's job, and signaling is yours. Security best practices goes into the gap.

What WebRTC does not give you

A realistic scope of the standard, because assuming otherwise is what turns a two-week estimate into a two-month project:

You get from the browserYou build or buy
Capture, echo cancellation, noise suppression, AGCDevice selection UI and permission recovery flows
Codec negotiation and encryptionSignaling transport, rooms, presence, auth
Congestion control and adaptive bitrateSTUN/TURN infrastructure, or a vendor for it
ICE connectivity establishmentSFU or MCU for anything past a handful of users
getStats() telemetryRecording, transcription, moderation, analytics pipeline

The right column is where the engineering time goes.

Where it breaks first — and how to check

Across the failures we have chased, the ranking is consistent: connectivity dominates. Media negotiation problems are real but comparatively rare and usually reproduce on your own machine; connectivity problems reproduce only on the user's network, which is exactly what makes them expensive.

The concrete failure modes worth recognizing on sight:

  • No srflx candidate. Outbound UDP is blocked, a VPN is swallowing the traffic, or the STUN server is down. Test a known-good public server to tell your network apart from your server.
  • No relay candidate. Credentials rejected or expired, turn: used where the server only speaks turns:, or the relay port range firewalled — the last being the failure a socket check will happily call "healthy."
  • Works for you, fails for a user. Almost always a network difference, which is why testing from your own laptop on office Wi-Fi proves very little.

Because these depend on the network the test runs from, a hosted checker in a clean data center will give you an optimistic answer. Running the check in the browser on the network in question is the only way to see what your users see. That is the reasoning behind this site's tester, and the full triage sequence lives in debugging WebRTC connections.

Where to go from here

If you are building your first WebRTC feature, the order that wastes the least time is: get getUserMedia() rendering locally, then stand up the dumbest possible signaling relay, then connect two tabs on the same machine, then — and this is the step people skip — connect two devices on different networks. Everything easy works at step three. Everything that ships gets decided at step four.

From there, read how ICE chooses a path before you read anything about codecs. Codec choice affects quality; ICE decides whether there is a call at all.

Check Your Own Servers

Now that you know how WebRTC negotiates a connection, make sure your STUN and TURN servers actually return candidates. Run them through our free tester to confirm they respond before you ship.

Open the Free ICE Server Tester →