WebRTC

WebRTC Data Channel: How RTCDataChannel Works, With Code Examples

16 min read
WebRTC
Reading Time: 12 minutes

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 a DATA_CHANNEL_OPEN message with the label, protocol, and reliability settings. The other side replies with DATA_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

  1. Create a peer connection on both sides with a list of STUN/TURN servers.
  2. Create a data channel on one side with createDataChannel(). This adds an m=application section to the SDP.
  3. Exchange the offer and answer through your WebRTC signaling server. WebRTC doesn’t define signaling, so you pick the transport (WebSocket, HTTP, anything).
  4. Exchange ICE candidates and let ICE pick the best path. This is where NAT traversal happens.
  5. Run the DTLS handshake over the chosen path.
  6. Start the SCTP association inside DTLS.
  7. Open the channel with DCEP. The initiator’s channel fires open, and the remote peer gets a datachannel event 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 attempts
  • maxPacketLifeTime: 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 to true
  • maxRetransmits: number of retries before a message is dropped
  • maxPacketLifeTime: milliseconds before a message is dropped
  • protocol: a string label for your app-level format, like "json" or "protobuf"
  • negotiated: set true to skip DCEP and agree on the channel out of band
  • id: the SCTP stream ID, required when negotiated is true

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:

  1. Create the channel before the offer. If you create it afterward, you’ll need to renegotiate, because the SDP won’t have an m=application section yet.
  2. Wait for open before calling send(). Sending while readyState is "connecting" throws an InvalidStateError.
  3. The receiver never calls createDataChannel(). It gets its end from the datachannel event.

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:

  1. 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.
  2. 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.

Join 200,000+ satisfied streamers

Still on the fence? Take a sneak peek and see what you can do with Castr.

No Castr Branding

No Castr Branding

We do not include our branding on your videos.

No Commitment

No Commitment

No contracts. Cancel or change your plans anytime.

24/7 Support

24/7 Support

Highly skilled in-house engineers ready to help.

  • Check Free 7-day trial
  • CheckCancel anytime
  • CheckNo credit card required