How RTCDataChannel Sends Data in JavaScript

The RTCDataChannel interface of the WebRTC API enables bidirectional, low-latency, peer-to-peer transfer of arbitrary data—including plain text, binary arrays, and raw blobs—directly between browsers without routing through an intermediary server. This article explores how RTCDataChannel works under the hood, how the underlying protocols manage transport, and how to implement it in JavaScript to send and receive custom data packets.

The Underlying Protocol Stack

Unlike standard media streams in WebRTC that rely on SRTP (Secure Real-time Transport Protocol), RTCDataChannel uses SCTP (Stream Control Transmission Protocol) encapsulated within DTLS (Datagram Transport Layer Security), which in turn runs over UDP managed by ICE (Interactive Connectivity Establishment).

This design gives developers granular control over delivery semantics, allowing the channel to behave reliably like TCP, unreliably like UDP, or in a semi-reliable hybrid mode.


Step 1: Establishing the Peer Connection

Before a data channel can open, two peers must negotiate a connection using an out-of-band signaling mechanism (such as WebSockets) to exchange SDP (Session Description Protocol) offers, answers, and ICE candidates.

const localConnection = new RTCPeerConnection(configuration);

Step 2: Creating the Data Channel

One peer initiates the channel by calling createDataChannel() on its RTCPeerConnection instance.

// Peer A (Initiator)
const dataChannel = localConnection.createDataChannel("chatChannel", {
  ordered: true, // Guarantees message delivery order
  // maxRetransmits: 3 // Optional: for unreliable/semi-reliable delivery
});

dataChannel.onopen = () => {
  console.log("Data channel is open and ready to transmit.");
};

dataChannel.onclose = () => {
  console.log("Data channel closed.");
};

The receiving peer listens for the channel creation via the ondatachannel event:

// Peer B (Receiver)
remoteConnection.ondatachannel = (event) => {
  const receiveChannel = event.channel;
  
  receiveChannel.onmessage = (event) => {
    console.log("Received data:", event.data);
  };
};

Step 3: Sending Arbitrary Data Packets

The RTCDataChannel.send() method is capable of transmitting various data types natively:

  1. Strings (UTF-8 Plain Text / JSON):

    dataChannel.send("Hello, Peer!");
    dataChannel.send(JSON.stringify({ type: "player_move", x: 10, y: 25 }));
  2. ArrayBuffer and TypedArrays (Binary Data):

    const buffer = new Uint8Array([0x01, 0xFF, 0x7F, 0x42]);
    dataChannel.send(buffer);
  3. Blobs (Raw File Chunks):

    const file = document.querySelector('input[type="file"]').files[0];
    dataChannel.send(file);

Step 4: Receiving and Parsing Data

To process binary data correctly on the receiving side, specify the binaryType property on the channel ('arraybuffer' or 'blob').

receiveChannel.binaryType = "arraybuffer";

receiveChannel.onmessage = (event) => {
  if (typeof event.data === "string") {
    const message = JSON.parse(event.data);
    console.log("Parsed JSON:", message);
  } else if (event.data instanceof ArrayBuffer) {
    const view = new Uint8Array(event.data);
    console.log("Binary packet received:", view);
  }
};

Managing Backpressure and Buffering

When sending large payloads or rapid streams of packets, data is placed into an internal sending buffer. If packets are queued faster than the network can transmit them, the buffer may overflow.

To prevent packet loss or excessive memory usage:

const CHUNK_SIZE = 16384; // 16 KB
dataChannel.bufferedAmountLowThreshold = 65536; // 64 KB

dataChannel.onbufferedamountlow = () => {
  sendNextChunk();
};

Reliability and Ordering Configuration

When calling createDataChannel(label, options), the options object allows customization of delivery guarantees:

Property Type Description
ordered boolean Set to false to deliver packets as soon as they arrive, regardless of sequence.
maxPacketLifeTime number The maximum time (in ms) to attempt packet delivery before dropping it.
maxRetransmits number The maximum number of retransmission attempts before failing.
negotiated boolean If true, avoids out-of-band channel negotiation if both sides pre-agree on an ID.
id number Explicit stream ID (0–65534) used when negotiated is set to true.