WebRTC Data Channels
WebRTC is best known for video calls, but the same technology can move arbitrary data directly between two browsers with no server in the path. That is what a data channel does: once two peers have found each other, they exchange messages over an encrypted peer-to-peer link whose latency is bounded only by the physical distance between them. For low-latency multiplayer state, direct file transfer, and mesh topologies, nothing a server-mediated transport offers can match it. The cost is a more involved setup, and understanding that setup is the whole game.
This guide builds on WebSocket fundamentals, which provides the signaling channel WebRTC depends on, and sits inside the Interactive Web cluster of realtime engineering patterns.
What you'll learn
Peer-to-peer beyond audio and video
The media side of WebRTC gets the attention, but the data channel is a general-purpose pipe. It carries whatever you put in it, directly between two clients.
A direct, encrypted pipe between two clients
A data channel sends text or binary messages over SCTP, which itself runs over DTLS, so every byte is encrypted without any extra work on your part. Because the connection is peer-to-peer, a message does not make a round trip to a server and back; it goes straight from one browser to the other. That removes a hop and, with it, latency and server cost. The trade-off is that establishing the connection is harder than opening a WebSocket, because the two peers are usually behind routers that hide their real addresses, and they have no way to find each other on their own. Solving that introduces signaling.
Signaling: SDP offer and answer
WebRTC deliberately leaves peer discovery to you. The specification covers the media and data transport but not how two browsers exchange the information needed to connect.
Exchanging session descriptions out of band
// Caller creates an offer and sends it over the signaling channel
var pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.example.com:3478' }]
});
var offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: offer.sdp });
// Callee receives the offer, answers, and sends it back
signaling.on('offer', async function (msg) {
await pc.setRemoteDescription({ type: 'offer', sdp: msg.sdp });
var answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signaling.send({ type: 'answer', sdp: answer.sdp });
});
Each peer produces a Session Description Protocol blob describing the media and data it supports. The caller sends an offer; the callee replies with an answer. This exchange must travel over a channel you provide, almost always the WebSocket connection covered earlier, because the peers cannot yet reach each other directly. The signaling server's only job is to relay these messages; once the peer connection is open, the server is no longer involved in the data flow.
ICE, STUN, and TURN
Knowing what a peer supports is not enough; you also need a network path to reach it. Most clients sit behind NAT, which rewrites addresses and blocks unsolicited inbound traffic. ICE is the framework that finds a working path.
Gathering candidates and traversing NAT
// Each discovered candidate is relayed to the other peer
pc.addEventListener('icecandidate', function (event) {
if (event.candidate) {
signaling.send({ type: 'ice', candidate: event.candidate });
}
});
signaling.on('ice', function (msg) {
pc.addIceCandidate(msg.candidate);
});
Interactive Connectivity Establishment gathers candidate addresses for each peer and tries them in order of preference until one works. A STUN server tells a peer its own public address as seen from the internet, which is enough for two peers to connect directly in most home and office networks. When direct connection is impossible, for example behind symmetric NAT or a strict corporate firewall, a TURN server relays the traffic through itself. TURN works everywhere but consumes server bandwidth for every byte, so it is a fallback, not a default. A production deployment configures both a STUN and a TURN server in the iceServers list and lets ICE pick the best path automatically.
The RTCDataChannel API
With signaling and ICE handled, the data channel itself is refreshingly simple, an event-driven object much like a WebSocket.
Opening a channel and sending messages
// Caller creates the channel before making the offer
var channel = pc.createDataChannel('game');
channel.addEventListener('open', function () {
channel.send(JSON.stringify({ type: 'join', name: 'Player 1' }));
});
channel.addEventListener('message', function (event) {
handle(JSON.parse(event.data));
});
// Callee receives the channel via the connection
pc.addEventListener('datachannel', function (event) {
var remote = event.channel;
remote.addEventListener('message', function (e) { handle(JSON.parse(e.data)); });
});
The caller creates the channel with createDataChannel before generating the offer; the callee receives it through the connection's datachannel event. After that, send and the message event mirror the WebSocket API exactly, which makes it straightforward to share message-handling code between a WebSocket fallback and a WebRTC primary path. Like WebSocket, a typed message envelope with a type field is the standard way to route the many kinds of events a single channel carries.
Reliable vs unreliable delivery
The single most useful feature of a data channel for realtime engineering is that you can turn off reliability. This is something a WebSocket cannot do.
Trading guarantees for latency
// Reliable + ordered (default): behaves like TCP
var chat = pc.createDataChannel('chat');
// Unreliable + unordered: behaves like UDP, ideal for live state
var state = pc.createDataChannel('state', {
ordered: false,
maxRetransmits: 0
});
By default a data channel is reliable and ordered, retransmitting lost packets and delivering them in sequence, exactly like TCP. That is correct for chat or file transfer where every byte matters. For continuously updated state, position, a cursor, a simulation tick, guaranteed delivery is the wrong trade: by the time a dropped packet is retransmitted, a newer update has already made it irrelevant. Setting ordered: false with maxRetransmits: 0 gives unreliable, unordered delivery like UDP, so the channel never waits on a lost packet and the freshest state always arrives as fast as the network allows. The realtime systems guide covers how to reconcile the gaps this creates.
Choosing WebRTC vs a server transport
WebRTC data channels are powerful but not free. The decision rests on topology and latency, not novelty.
When direct peer connection earns its complexity
Choose WebRTC when peers benefit from talking directly: low-latency multiplayer state synchronization, direct file transfer that should not pass through your servers, or small mesh groups where every participant connects to every other. Choose a server transport such as WebSocket or Server-Sent Events when the server must see every message anyway, when you need to broadcast to many clients, or when the operational cost of running STUN and TURN servers is not justified. Many production systems use both: WebSocket for signaling and authoritative state, WebRTC for the latency-critical peer-to-peer path. The remaining guides in this cluster build the synchronization and reconciliation patterns that sit on top of whichever transport you choose.
Frequently Asked Questions
What is an RTCDataChannel?
An RTCDataChannel is a bidirectional peer-to-peer channel for sending arbitrary text or binary data directly between two browsers over a WebRTC connection. It runs on SCTP over DTLS, so traffic is encrypted by default, and it can be configured for reliable or unreliable delivery.
Why does WebRTC need a signaling server?
WebRTC does not define how two peers find each other. Before a peer-to-peer connection can open, the peers must exchange SDP offer and answer descriptions and ICE candidates over a separate channel, commonly a WebSocket connection. That exchange is called signaling, and the application is responsible for it.
What is the difference between STUN and TURN?
A STUN server helps a peer discover its public IP address and port so two peers can try to connect directly. A TURN server relays traffic through itself when a direct path cannot be established, for example behind symmetric NAT. TURN consumes server bandwidth, so it is used only as a fallback.
Can a data channel be unreliable like UDP?
Yes. By default a data channel is reliable and ordered like TCP, but setting ordered to false with maxRetransmits or maxPacketLifeTime gives unordered, unreliable delivery like UDP. This suits real-time state where the latest update matters more than guaranteed delivery of every packet.
Read next: the Interactive Web hub, or continue to Server-Sent Events for a server-push transport comparison.
Want the data layer that realtime messages update?
The Backbone guide covers the model and collection layer behind any realtime frontend.
Explore the Backbone Guide →