Most people meet WebRTC through video calls. But the same peer connection that carries your camera feed can also carry anything else: chat messages, game state, cursor positions, files, sensor readings.
That’s the WebRTC data channel. It’s a browser API that sends arbitrary text and binary data directly between two peers, encrypted by default, with delivery rules you pick per channel.
You can make one channel behave like TCP and the next one behave like UDP, on the same connection, with a single line of config.
No WebSocket server sits in the middle relaying every message.
What Is a WebRTC Data Channel?
A WebRTC data channel is a bidirectional, peer-to-peer transport for arbitrary application data, exposed in browsers as the RTCDataChannel interface and carried over SCTP inside an encrypted DTLS connection.
You create it from an existing RTCPeerConnection, the same object that handles audio and video in WebRTC. Once it opens, both sides can call send() and listen for message events, much like a WebSocket.
The difference is the path. Data goes straight from one peer to the other whenever the network allows it, so there’s no application server in the loop and no extra hop adding latency.
The protocol is standardized in RFC 8831, and every major browser ships it: Chrome, Firefox, Safari, and Edge.
Here are the core properties at a glance:
| Property | WebRTC data channel |
|---|---|
| Browser API | RTCDataChannel (created by RTCPeerConnection.createDataChannel()) |
| Topology | Peer to peer (can also terminate on a server) |
| Transport stack | SCTP over DTLS over UDP (with ICE) |
| Encryption | Mandatory DTLS, can’t be turned off |
| Data types | Strings, ArrayBuffer, Blob, typed arrays |
| Delivery modes | Reliable or partially reliable, ordered or unordered |
| Channels per connection | Up to 65,534 |
| Setup requirement | Signaling to exchange SDP offer/answer and ICE candidates |
One point confuses newcomers: a data channel isn’t a standalone connection. It lives inside a peer connection, so you still need the whole WebRTC setup process first.
The payoff? You can add dozens of channels to that connection for almost no extra cost.
How Does a WebRTC Data Channel Work?
A WebRTC data channel runs on a layered stack, and each layer solves one problem. Know the layers and you’ll understand most of the channel’s behavior.
+--------------------------------------+
| Your app: send() / onmessage |
+--------------------------------------+
| DCEP (channel open/ack, RFC 8832) |
+--------------------------------------+
| SCTP (streams, reliability, CC) |
+--------------------------------------+
| DTLS (encryption, authentication) |
+--------------------------------------+
| ICE (STUN/TURN path selection) |
+--------------------------------------+
| UDP |
+--------------------------------------+
That’s the “SCTP over DTLS over UDP” stack you’ll see in every spec and diagram.
The protocol layers
- UDP carries the packets. It’s fast and doesn’t block on lost packets, which is why real-time media uses it.
- ICE finds a working network path between the peers. It gathers candidates from local interfaces, a STUN server (your public address), and a TURN server (a relay for when direct paths fail).
- DTLS encrypts and authenticates everything. It’s TLS adapted for datagrams, and the certificate fingerprints travel in the SDP so each peer knows who it’s talking to.
- SCTP (Stream Control Transmission Protocol) turns the encrypted pipe into many independent message streams. It handles retransmission, ordering, fragmentation, flow control, and congestion control.
- DCEP (Data Channel Establishment Protocol) opens channels in-band. When you call
createDataChannel(), the browser sends aDATA_CHANNEL_OPENmessage with the label, protocol, and reliability settings. The other side replies withDATA_CHANNEL_ACK.
SCTP is the interesting choice. It was designed for telephony signaling and gives you things TCP can’t: message boundaries (you receive exactly the messages that were sent, not a byte stream) and multiple streams that don’t block each other.
Each data channel maps to one SCTP stream ID. A lost packet on your file-transfer channel won’t stall your chat channel.
The connection sequence, step by step
- Create a peer connection on both sides with a list of STUN/TURN servers.
- Create a data channel on one side with
createDataChannel(). This adds anm=applicationsection to the SDP. - Exchange the offer and answer through your WebRTC signaling server. WebRTC doesn’t define signaling, so you pick the transport (WebSocket, HTTP, anything).
- Exchange ICE candidates and let ICE pick the best path. This is where NAT traversal happens.
- Run the DTLS handshake over the chosen path.
- Start the SCTP association inside DTLS.
- Open the channel with DCEP. The initiator’s channel fires
open, and the remote peer gets adatachannelevent holding its end of the channel.
Steps 5 through 7 usually finish in a few round trips. Most of the setup time goes into signaling and ICE.
WebRTC Data Channel Reliability Modes
This is the feature that sets the WebRTC data channel apart from WebSocket. You choose delivery guarantees per channel, using two independent settings.
Ordering controls whether messages arrive in the order they were sent. Set ordered: false and a late message won’t hold up the ones behind it.
Reliability controls whether lost messages get retransmitted. By default they do, forever, until they arrive. You can cap that with one of two options:
maxRetransmits: give up after N retransmission attemptsmaxPacketLifeTime: give up after N milliseconds
You can set one or the other, never both. Setting both throws a TypeError.
| Mode | Config | Behaves like | Good for |
|---|---|---|---|
| Reliable, ordered (default) | {} |
TCP | Chat, commands, file transfer |
| Reliable, unordered | { ordered: false } |
TCP without head-of-line blocking | Independent file chunks, bulk sync |
| Partially reliable, by retries | { ordered: false, maxRetransmits: 0 } |
UDP | Game state, cursor position, telemetry |
| Partially reliable, by time | { ordered: false, maxPacketLifeTime: 150 } |
UDP with a short retry window | Controller input, live annotations |
The rule of thumb: if a newer message makes an older one useless, use an unordered, unreliable channel.
A player’s position from 100 ms ago doesn’t matter once you have the current one. Retransmitting it only adds lag, the same way retransmits add video latency to a live stream.
A chat message is different. It has to arrive, and in order.
Many apps open two channels on one connection: a reliable one for events and an unreliable one for high-frequency state.
The RTCDataChannel API
The RTCDataChannel API is small. Most of what you need fits on one screen.
Creating a channel
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
const channel = pc.createDataChannel("chat", {
ordered: true, // default
// maxRetransmits: 0, // or maxPacketLifeTime, not both
protocol: "json", // optional subprotocol label
});
The options object accepts:
ordered: boolean, defaults totruemaxRetransmits: number of retries before a message is droppedmaxPacketLifeTime: milliseconds before a message is droppedprotocol: a string label for your app-level format, like"json"or"protobuf"negotiated: settrueto skip DCEP and agree on the channel out of bandid: the SCTP stream ID, required whennegotiatedistrue
Automatic vs negotiated channels
By default, one side creates the channel and the other side receives it:
// Remote peer
pc.addEventListener("datachannel", (event) => {
const channel = event.channel;
channel.onmessage = (e) => console.log("Got:", e.data);
});
With negotiated: true, both sides create the channel themselves with the same id. No datachannel event fires:
// Run on BOTH peers
const control = pc.createDataChannel("control", { negotiated: true, id: 0 });
Negotiated channels skip the DCEP round trip and let you set up channels symmetrically. They’re common in multiplayer games and in server-side stacks where both ends are under your control.
Properties and events worth knowing
| Name | Type | What it tells you |
|---|---|---|
readyState |
property | "connecting", "open", "closing", or "closed" |
bufferedAmount |
property | Bytes queued but not yet sent |
bufferedAmountLowThreshold |
property | Byte level that fires bufferedamountlow |
binaryType |
property | "arraybuffer" or "blob" for incoming binary data |
label / id |
property | Channel name and SCTP stream ID |
open / close |
event | Channel is ready / has shut down |
message |
event | Data arrived (event.data) |
bufferedamountlow |
event | Send queue dropped below your threshold |
error |
event | Something failed, such as a message dropped on close |
Set binaryType explicitly. Browser defaults have differed over the years, and setting "arraybuffer" saves you from debugging Blob objects you didn’t expect.
WebRTC Data Channel Example: A Simple Chat
Here’s a complete, simple WebRTC datachannel chat example. To keep it runnable in one tab, both peers live on the same page and “signaling” is just passing objects between them. In a real app, you’d send the SDP and candidates over your signaling server.
<input id="msg" placeholder="Type a message" />
<button id="send">Send</button>
<pre id="log"></pre>
<script>
const log = (t) => (document.querySelector("#log").textContent += t + "\n");
const peerA = new RTCPeerConnection();
const peerB = new RTCPeerConnection();
// "Signaling": hand ICE candidates straight to the other peer
peerA.onicecandidate = (e) => e.candidate && peerB.addIceCandidate(e.candidate);
peerB.onicecandidate = (e) => e.candidate && peerA.addIceCandidate(e.candidate);
// Peer A creates the channel
const chatA = peerA.createDataChannel("chat");
chatA.onopen = () => log("Channel open");
chatA.onmessage = (e) => log("A got: " + e.data);
// Peer B receives it
peerB.ondatachannel = (e) => {
const chatB = e.channel;
chatB.onmessage = (msg) => {
log("B got: " + msg.data);
chatB.send("echo: " + msg.data);
};
};
// Offer/answer exchange
(async () => {
await peerA.setLocalDescription(await peerA.createOffer());
await peerB.setRemoteDescription(peerA.localDescription);
await peerB.setLocalDescription(await peerB.createAnswer());
await peerA.setRemoteDescription(peerB.localDescription);
})();
document.querySelector("#send").onclick = () => {
const input = document.querySelector("#msg");
if (chatA.readyState === "open") chatA.send(input.value);
input.value = "";
};
</script>
Three details make this work:
- Create the channel before the offer. If you create it afterward, you’ll need to renegotiate, because the SDP won’t have an
m=applicationsection yet. - Wait for
openbefore callingsend(). Sending whilereadyStateis"connecting"throws anInvalidStateError. - The receiver never calls
createDataChannel(). It gets its end from thedatachannelevent.
To split this across two machines, swap the direct calls for messages over a WebSocket. If you’re building the UI in a framework, our guides to WebRTC in React and React Native WebRTC show where the peer connection logic belongs.
Sending Files and Binary Data Over a Data Channel
Files are where most data channel bugs show up. Chat messages are tiny; files aren’t.
Two limits matter: maximum message size and the send buffer.
Maximum message size
Each peer advertises how big a single message it can receive, using the max-message-size attribute in the SDP, defined in RFC 8841. If the attribute is missing, the default is 64 KB.
Most current browsers accept messages of at least 256 KB. You can read the negotiated value from pc.sctp.maxMessageSize once the connection is up.
Still, don’t send huge single messages. Without SCTP message interleaving, one large message blocks every other channel on the connection until it finishes. That’s head-of-line blocking across channels, the exact thing SCTP streams are supposed to prevent.
The safe pattern is to chunk. 16 KB chunks work across every browser and keep other channels responsive.
Handling backpressure
send() never blocks. It queues data and returns right away, so a loop that calls send() on a 2 GB file will queue all 2 GB in memory and can crash the tab.
Use bufferedAmount and the bufferedamountlow event to pace yourself:
const CHUNK = 16 * 1024; // 16 KB
const HIGH_WATER = 1024 * 1024; // pause above 1 MB queued
async function sendFile(channel, file) {
channel.binaryType = "arraybuffer";
channel.bufferedAmountLowThreshold = 256 * 1024;
channel.send(JSON.stringify({ name: file.name, size: file.size }));
let offset = 0;
while (offset < file.size) {
if (channel.bufferedAmount > HIGH_WATER) {
await new Promise((resolve) =>
channel.addEventListener("bufferedamountlow", resolve, { once: true })
);
}
const buf = await file.slice(offset, offset + CHUNK).arrayBuffer();
channel.send(buf);
offset += buf.byteLength;
}
channel.send(JSON.stringify({ done: true }));
}
On the receiving side, collect chunks into an array and build a Blob when the done message arrives. Because the channel is reliable and ordered by default, the chunks show up in sequence.
For multi-gigabyte transfers, add a checksum per file and a resume offset so a dropped connection doesn’t mean starting over.
WebRTC Data Channel vs WebSocket
WebRTC data channels and WebSockets solve different problems, even though both give you a bidirectional message pipe in the browser. That’s why “WebRTC data channel vs WebSocket” is one of the most searched questions on this topic.
| Factor | WebRTC data channel | WebSocket |
|---|---|---|
| Topology | Peer to peer | Client to server |
| Transport | SCTP over DTLS over UDP | TCP (TLS optional via wss://) |
| Delivery | Configurable: reliable or lossy, ordered or not | Always reliable and ordered |
| Head-of-line blocking | Avoidable per channel | Yes, one lost packet stalls everything |
| Encryption | Always on (DTLS) | Only with wss:// |
| Setup | Signaling + ICE + DTLS + SCTP | One HTTP upgrade |
| Firewall friendliness | Needs TURN on strict networks | Works almost everywhere on port 443 |
| Server cost for relaying | None on direct paths | Server handles every message |
| Best fit | Games, P2P file transfer, low-latency sync | Notifications, chat backends, signaling |
The irony is that you usually need both. WebRTC has no built-in signaling, and a WebSocket is the most common way to deliver offers, answers, and ICE candidates.
So the WebSocket handles setup and server-driven events, and the data channel carries the high-frequency, latency-sensitive traffic between peers. For a deeper look, read our full comparison of WebRTC vs WebSocket.
Common WebRTC Data Channel Use Cases
Data channels show up anywhere two clients need to talk fast without a server paying for every byte.
- Multiplayer browser games. Unreliable, unordered channels carry player input and state snapshots at 20 to 60 updates per second.
- Peer-to-peer file sharing. Apps like ShareDrop send files browser to browser, so the server never stores them.
- Collaborative editing. Cursor positions, selections, and CRDT operations sync between editors.
- Remote control and screen annotation. Mouse and keyboard events ride alongside a video track, a common pairing with WebRTC screen sharing.
- In-call features. Chat, reactions, hand raises, and captions in video calls, without a separate messaging service.
- IoT and robotics. Microcontrollers and robots stream telemetry and receive commands with low delay.
- Interactive live video. Polls, quizzes, bids, and product clicks tied to a live stream, the core of interactive video.
Limitations of WebRTC Data Channels
Data channels are fast and flexible. They also come with real trade-offs.
Connection setup is heavy
Before a single byte flows, you need signaling, ICE, a DTLS handshake, and an SCTP association. That’s fine for a call that lasts minutes. It’s overkill for a one-off notification.
Mitigation: keep the peer connection alive and reuse it, and open new channels on it instead of new connections.
Some networks block direct paths
Symmetric NATs and strict corporate firewalls can defeat STUN. Then traffic has to go through a TURN relay, which costs bandwidth and adds a hop.
Mitigation: always configure TURN, including TURN over TLS on port 443, as a fallback.
Peer to peer doesn’t scale to large groups
In a mesh, every peer connects to every other peer. With 10 users, that’s 45 connections, and each client sends every message 9 times.
Mitigation: route data through a server, covered in the next section.
Message size and buffering take manual work
As shown above, you have to chunk large payloads and manage backpressure yourself. The API won’t do it for you.
Debugging is harder than HTTP
Traffic is encrypted and spread across several protocols. Browser tools like chrome://webrtc-internals and about:webrtc in Firefox help, but they’re less familiar than a network tab.
Building With Data Channels in Production
Everything so far works fine for a demo between two browser tabs. Production adds three questions: how you scale past a handful of peers, what runs on the server side, and how data channels fit with the video you’re delivering.
Scaling WebRTC Data Channels Beyond Peer to Peer
Once groups get bigger than 4 to 6 people, most teams move data channel traffic onto a server.
That server terminates each client’s peer connection and forwards messages, the same model a selective forwarding unit (SFU) uses for media. Each client keeps one connection, and the server fans messages out.
You give up the “no server” property, but you keep everything else: UDP transport, per-channel reliability modes, and DTLS encryption. The server can also enforce permissions and log events, which a pure mesh can’t do.
Server-side libraries that speak data channels include:
- libdatachannel: a lightweight C/C++ library for data channels, media transport, and WebSockets
- Pion: a pure Go WebRTC implementation, popular for SFUs and custom servers
- aiortc: an asyncio WebRTC stack for Python (see our guide to WebRTC with Python)
- Google’s libwebrtc: the native library behind Chrome, also used for WebRTC data channels on Android and iOS
If you’re choosing infrastructure for this, our overview of the WebRTC server types (signaling, STUN/TURN, SFU, MCU) walks through what each one does.
Pairing Data Channels With Live Video Delivery
Here’s a pattern that trips up a lot of teams. WebRTC data channels are great for interactive data among a small group. WebRTC media is great for sub-second video among a small group.
Neither is built for sending one live stream to 50,000 viewers.
For large audiences, the usual architecture splits the job in two:
- Video goes out over HLS through a CDN. It scales to any audience size and plays on every device. Read what HLS streaming is for the details.
- Interaction goes over a real-time channel. Chat, reactions, bids, and polls run on data channels (for small groups or via a server) or WebSockets (for huge fan-out).
That’s the setup behind live shopping platforms, live auctions, and watch parties. The video can tolerate a few seconds of delay. The “buy now” button can’t.
LiveAPI handles the video side. The LiveAPI live streaming API takes RTMP or SRT ingest, transcodes with adaptive bitrate, and delivers HLS through Akamai, Cloudflare, and Fastly, with up to 4K output and an embeddable player.
With live to VOD, every stream is recorded automatically, so the replay is ready the moment the event ends. Your team builds the interactive layer on data channels, and you don’t spend months building video infrastructure.
If you need the video itself to be sub-second, see our guide to WebRTC live streaming and the trade-offs of ultra-low latency video streaming.
WebRTC Data Channel FAQ
Is a WebRTC data channel TCP or UDP?
Neither directly. A WebRTC data channel runs SCTP over DTLS, which runs over UDP. SCTP can act like TCP (reliable, ordered) or like UDP (unreliable, unordered), depending on the options you pass to createDataChannel().
Is WebRTC data channel encrypted?
Yes, always. All data channel traffic is encrypted with DTLS, and the spec doesn’t allow turning it off. The DTLS certificate fingerprints are exchanged in the SDP, so peers can verify each other.
Do WebRTC data channels need a server?
You need a signaling server to exchange SDP and ICE candidates, and usually STUN and TURN servers for connectivity. Once connected, the data itself can flow directly between peers with no server in the path.
What is the maximum message size for a WebRTC data channel?
It’s negotiated through the max-message-size SDP attribute, with a 64 KB default when the attribute is absent. Most modern browsers accept at least 256 KB. For cross-browser safety, chunk large payloads into 16 KB messages.
How many data channels can one peer connection have?
Up to 65,534, one per SCTP stream ID. In practice, apps rarely use more than a handful. Opening a new channel on an existing connection is cheap because it reuses the same DTLS and SCTP association.
Can I use a WebRTC data channel without audio or video?
Yes. A peer connection can carry only data channels. The SDP will contain a single m=application section for SCTP and no media sections.
What’s the difference between RTCDataChannel and RTCPeerConnection?
RTCPeerConnection manages the whole connection between two peers: signaling state, ICE, DTLS, and media tracks. RTCDataChannel is one message stream created on top of that connection with createDataChannel().
Do WebRTC data channels work on Android and iOS?
Yes. Mobile browsers support RTCDataChannel, and native apps can use Google’s libwebrtc or wrappers like React Native WebRTC. The API surface on native platforms mirrors the browser version closely.
Start Building With WebRTC Data Channels
The WebRTC data channel gives you something no other browser API does: encrypted, peer-to-peer messaging with delivery rules you choose per channel. Use reliable channels for anything that must arrive, unreliable ones for state that goes stale fast, and chunk anything bigger than 16 KB.
Pair it with a signaling server for setup, a TURN server for tough networks, and a server-side relay once groups grow. When your app also needs live video for a large audience, let a video API handle delivery while your data channels handle the interaction.
Get started with LiveAPI to add live streaming, HLS delivery, and live-to-VOD recording to your app in days instead of months.
