WebRTC & WebTransport
A video call where the two people are one room apart shouldn’t route every frame through a server in another country. A multiplayer game can’t wait for a reliable, in-order channel to re-send a dropped position update — by the time it arrives, the player has already moved. Both cases want something plain HTTP and even WebSocket don’t give you: a choice about reliability, and sometimes a path that skips the server entirely.
Two APIs cover this ground. WebRTC connects browsers directly to each other for peer-to-peer audio, video, and arbitrary data. WebTransport is a newer client-to-server channel that runs over HTTP/3 and lets you send unreliable datagrams alongside reliable, independently-flowing streams. They solve different problems, and the fastest way to understand each is to see what it’s built on.
WebRTC: connecting two browsers directly
The headline feature of WebRTC is that data can travel directly between two browsers, without passing through your server. Lower latency, and your infrastructure doesn’t carry the media bytes. But two browsers on home networks can’t just find each other — each one is usually behind a router doing NAT (Network Address Translation), with no public address a stranger can dial. Getting them connected is the hard part, and WebRTC splits that work into two halves.
The important thing to sit with: WebRTC does not ship a signalling protocol. It hands you strings to exchange and expects you to move them. Any channel works — a WebSocket, a couple of fetch calls, even copy-paste. That flexibility trips people up, because “set up a call” turns out to require a small server of your own just to introduce the peers.
The offer / answer dance
Each side of a connection is an RTCPeerConnection. One peer starts as the caller and creates an offer — a blob of SDP (Session Description Protocol) text describing what it wants to send and the codecs it supports. The other peer, the callee, replies with an answer. You relay both across your signalling channel.
// Caller
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: "offer", sdp: offer.sdp });
createOffer() returns a description; setLocalDescription() commits it and, as a side effect, kicks the browser’s ICE agent into gathering network candidates. The callee does the mirror image:
// Callee, on receiving the offer
const pc = new RTCPeerConnection({ iceServers: [/* ... */] });
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signaling.send({ type: "answer", sdp: answer.sdp });
Back on the caller, receiving that answer finishes the description exchange with await pc.setRemoteDescription(answer). Now both sides know what the other wants to send. What they still don’t know is how to reach each other.
ICE, STUN, and TURN: finding a path through NAT
While the SDP travels, the ICE agent (Interactive Connectivity Establishment) is busy collecting candidates — possible addresses this peer might be reachable on. There are a few kinds:
- Host candidates: the machine’s own local addresses (like
192.168.1.20). Useful only if both peers are on the same network. - Server-reflexive candidates: your public address as seen from the outside, discovered by asking a STUN server “what address did my packet arrive from?” This is what lets two peers behind different routers find a direct path.
- Relay candidates: an address on a TURN server that will forward traffic for you when no direct path exists. Slower and it costs you bandwidth, but it works when everything else fails.
Each candidate your browser finds fires an icecandidate event. You forward it to the other peer, who feeds it in with addIceCandidate(). This trickle happens continuously, in parallel with the offer/answer, so the connection comes up as soon as any candidate pair works.
pc.addEventListener("icecandidate", (event) => {
if (event.candidate) {
signaling.send({ type: "candidate", candidate: event.candidate });
}
});
// On the other side, when a candidate arrives over signalling:
await pc.addIceCandidate(incoming.candidate);
Pulling in the camera and mic
For a call you need media, and that comes from getUserMedia. It prompts the user for permission and resolves to a MediaStream whose tracks you add to the connection.
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
// Show your own preview
localVideo.srcObject = stream;
// Send each track to the peer
for (const track of stream.getTracks()) {
pc.addTrack(track, stream);
}
On the far side, tracks the peer sends surface through a track event, and you attach them to a <video>:
pc.addEventListener("track", (event) => {
remoteVideo.srcObject = event.streams[0];
});
The data channel: arbitrary bytes, your reliability rules
Media is only half of WebRTC. RTCDataChannel gives you a peer-to-peer pipe for anything — game state, file chunks, chat, cursor positions. You open one from either peer:
const channel = pc.createDataChannel("game", {
ordered: false,
maxRetransmits: 0,
});
channel.addEventListener("open", () => channel.send("ready"));
channel.addEventListener("message", (e) => applyUpdate(e.data));
The other peer receives it through a datachannel event:
pc.addEventListener("datachannel", (event) => {
const channel = event.channel;
channel.addEventListener("message", (e) => applyUpdate(e.data));
});
The interesting knobs are ordered and the retransmit controls. Under the hood a data channel runs over SCTP, which — unlike TCP — lets you dial reliability down. Two options tune it, and you may set at most one of them (passing both throws a TypeError):
maxRetransmits— how many times to re-send a lost message before giving up.maxPacketLifeTime— how many milliseconds to keep trying before giving up.
For a shooter, “unreliable + unordered” is exactly right: a position from 80ms ago is useless once a newer one exists, so re-sending it would only delay the fresh one. For file transfer you want the default reliable, ordered mode. The point is that WebRTC lets you choose, which TCP-based transports can’t.
WebTransport: low latency to your server over HTTP/3
WebRTC is peer-to-peer and complex. Plenty of real-time apps don’t need peers to talk directly — they just need a fast, flexible pipe to your server. That’s WebTransport’s slot. It’s a client-to-server API built on HTTP/3, which itself runs over QUIC, a transport on top of UDP.
Why does the underlying protocol matter here? Because QUIC fixes the flaw that bites WebSocket under packet loss: head-of-line blocking.
Head-of-line blocking, and why HTTP/3 dodges it
A WebSocket is a single TCP connection. TCP guarantees in-order delivery, which sounds good until one packet is lost. Everything sent after the lost packet has to wait in a buffer until the missing one is re-sent — even if those later bytes belong to a completely unrelated message. One dropped packet stalls the whole pipe.
QUIC runs many independent streams over one connection. A loss on stream A holds up only stream A; streams B and C keep flowing. That’s the multiplexing WebSocket never had.
The API shape
You open a connection with a URL. ready resolves once it’s usable; closed resolves (or rejects) when it ends.
const transport = new WebTransport("https://example.com:4433/live");
await transport.ready;
From there you have two ways to send, matching the two things a real-time app usually wants.
Reliable streams — like a WebRTC data channel in its default mode, but to a server. Create one, get a writable/readable pair, and use the Streams API to move bytes. Each stream is independent, so several can be in flight without blocking each other.
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode("hello"));
await writer.close();
const reader = stream.readable.getReader();
const { value } = await reader.read();
Datagrams — unreliable, unordered, no delivery guarantee. There’s no per-message stream object; you read and write through the single datagrams duplex. This is the “send it and move on” path for game ticks or live telemetry.
const writer = transport.datagrams.writable.getWriter();
await writer.write(new Uint8Array([0x01, 0x02]));
const reader = transport.datagrams.readable.getReader();
const { value } = await reader.read(); // a Uint8Array, or nothing — no guarantee
Where WebTransport stands in mid-2026
This is the newest of the three transports, and its status genuinely changed recently, so be precise about it. WebTransport is a W3C specification. Chromium browsers (Chrome, Edge, Opera) have shipped it since 2021–2022, and Firefox since version 114. The last gap was Safari — and it closed in Safari 26.4, released March 2026. With that, WebTransport became Baseline “newly available” in 2026: supported across the current versions of every major browser.
Peer-to-peer or client-server?
The deepest difference between these two isn’t the API — it’s the shape of the network they build. WebRTC connects peers to each other. WebTransport connects clients to your server. That choice ripples into latency, cost, and how many participants you can support.
That mesh is why large WebRTC calls don’t actually stay peer-to-peer. Past a handful of participants, every peer sending to every other peer saturates upstream bandwidth, so production video platforms route through a media server (an SFU) that each peer connects to once — trading the pure-P2P latency win for something that scales. For a two-person call, direct P2P is ideal.
A rough decision rule: if the data must go between users, it’s WebRTC. If it goes between a user and you, start with WebSocket and step up to WebTransport when its stream multiplexing or datagrams earn their keep.
Summary
- WebRTC links two browsers directly. You provide the signalling channel to swap an offer, an answer, and ICE candidates; the browser’s ICE agent uses STUN to discover public addresses and TURN to relay when no direct path exists.
RTCPeerConnectiondrives the connection;getUserMediasupplies camera/mic tracks viaaddTrack; incoming media arrives on thetrackevent.RTCDataChannelis a P2P pipe for arbitrary bytes with tunable reliability — setorderedplus eithermaxRetransmitsormaxPacketLifeTime(never both) to trade guaranteed delivery for lower latency.- WebTransport is a client-to-server API over HTTP/3 / QUIC. It offers reliable, independently-multiplexed streams (no head-of-line blocking) and unreliable datagrams, all in terms of
Uint8Array. - WebTransport became Baseline “newly available” in 2026 once Safari 26.4 shipped in March 2026 — current browsers support it, but older clients don’t, so feature-detect and keep a fallback.
- Match the transport to the network shape: P2P and live media → WebRTC, low-latency client↔server → WebTransport, simple reliable bidirectional → WebSocket.