WebRTC Cheatsheet

Data Channels

Use this WebRTC reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Creating a Data Channel

// Caller creates the channel before the offer
const dc = pc.createDataChannel('chat', {
  ordered: true,          // guarantee delivery order (like TCP)
  maxRetransmits: null,   // unlimited retransmits (omit for reliable)
  // OR:
  // maxPacketLifetime: 3000, // ms — unreliable, time-bounded
  // maxRetransmits: 0,       // unreliable, fire-and-forget
});

You can only use maxPacketLifetime or maxRetransmits, never both. Omitting both means reliable/ordered (default).

Receiving a Data Channel

// Callee — listen for the channel the caller created
pc.ondatachannel = ({ channel }) => {
  setupChannel(channel);
};

Channel Configuration Options

OptionTypeDefaultDescription
orderedbooleantrueGuarantee in-order delivery
maxPacketLifetimenumbernullms before giving up on a message
maxRetransmitsnumbernullRetransmit attempts before dropping
protocolstring""Application-level sub-protocol name
negotiatedbooleanfalseIf true, both sides create the channel manually
idnumberautoStream ID for negotiated channels

Reliability Modes

ModeorderedmaxRetransmitsmaxPacketLifetimeLike
Reliable ordered (default)truenonenoneTCP
Reliable unorderedfalsenonenoneTCP but unordered
Unreliable orderedtrue0
Unreliable unorderedfalse0UDP
Partially reliableeitherNSCTP PR

Data Channel Events

function setupChannel(dc) {
  dc.onopen = () => {
    console.log('Channel open, label:', dc.label, 'id:', dc.id);
    dc.send('Hello!');
  };

  dc.onclose = () => {
    console.log('Channel closed');
  };

  dc.onerror = (event) => {
    console.error('Channel error:', event.error);
  };

  dc.onmessage = ({ data }) => {
    if (typeof data === 'string') {
      console.log('Text message:', data);
    } else {
      // ArrayBuffer or Blob
      const view = new Uint8Array(data);
      console.log('Binary message, bytes:', view.length);
    }
  };

  dc.onbufferedamountlow = () => {
    // Safe to resume sending after back-pressure
    resumeSending();
  };
}

Sending Data

// Text
dc.send('Hello, world!');

// JSON
dc.send(JSON.stringify({ type: 'move', x: 100, y: 200 }));

// ArrayBuffer (raw binary)
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setFloat32(0, 3.14);
dc.send(buf);

// TypedArray
dc.send(new Uint8Array([1, 2, 3, 4]));

// Blob
const blob = new Blob(['some data'], { type: 'text/plain' });
dc.send(blob);

Receiving Binary — binaryType

// Default is 'blob' — switch to 'arraybuffer' for easier processing
dc.binaryType = 'arraybuffer'; // 'blob' | 'arraybuffer'

dc.onmessage = ({ data }) => {
  if (data instanceof ArrayBuffer) {
    const view = new DataView(data);
    const x = view.getFloat32(0);
  } else if (data instanceof Blob) {
    data.arrayBuffer().then(buf => { /* process */ });
  }
};

Back-Pressure / Flow Control

const BUFFER_THRESHOLD = 64 * 1024; // 64 KB

dc.bufferedAmountLowThreshold = BUFFER_THRESHOLD;

function sendChunk(data) {
  if (dc.bufferedAmount > BUFFER_THRESHOLD) {
    // Pause; resume in onbufferedamountlow
    pendingData.push(data);
    return;
  }
  dc.send(data);
}

dc.onbufferedamountlow = () => {
  while (pendingData.length && dc.bufferedAmount <= BUFFER_THRESHOLD) {
    dc.send(pendingData.shift());
  }
};
PropertyTypeDescription
dc.bufferedAmountnumberBytes queued but not yet sent
dc.bufferedAmountLowThresholdnumberonbufferedamountlow fires when bufferedAmount drops below this

File Transfer Over a Data Channel

// Sender
async function sendFile(dc, file) {
  const CHUNK_SIZE = 16 * 1024; // 16 KB chunks
  dc.send(JSON.stringify({ type: 'file-meta', name: file.name, size: file.size }));

  let offset = 0;
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + CHUNK_SIZE);
    const buf = await chunk.arrayBuffer();

    // Wait if buffer is filling up
    if (dc.bufferedAmount > 4 * CHUNK_SIZE) {
      await new Promise(r => { dc.onbufferedamountlow = r; });
    }

    dc.send(buf);
    offset += CHUNK_SIZE;
  }
  dc.send(JSON.stringify({ type: 'file-end' }));
}

// Receiver
const chunks = [];
let meta = null;

dc.binaryType = 'arraybuffer';
dc.onmessage = ({ data }) => {
  if (typeof data === 'string') {
    const msg = JSON.parse(data);
    if (msg.type === 'file-meta') {
      meta = msg;
      chunks.length = 0;
    } else if (msg.type === 'file-end') {
      const blob = new Blob(chunks);
      const url = URL.createObjectURL(blob);
      // trigger download
      Object.assign(document.createElement('a'), { href: url, download: meta.name }).click();
    }
  } else {
    chunks.push(data);
  }
};

Pre-Negotiated Channels

Both peers create the channel with negotiated: true and the same id, bypassing the in-band negotiation. Useful when you want channels available before the first message is sent.

// Both peers run this identically — no ondatachannel needed
const dc = pc.createDataChannel('reliable', { negotiated: true, id: 0 });
const dcUnreliable = pc.createDataChannel('game-state', {
  negotiated: true,
  id: 1,
  ordered: false,
  maxRetransmits: 0,
});

Data Channel States

dc.readyState; // 'connecting' | 'open' | 'closing' | 'closed'

// Only send when open
if (dc.readyState === 'open') dc.send(data);

Multiple Channels Per Connection

// Each addtrack and createDataChannel shares the same underlying SCTP transport
const chat   = pc.createDataChannel('chat');
const files  = pc.createDataChannel('files',  { ordered: true });
const telemetry = pc.createDataChannel('telemetry', { ordered: false, maxRetransmits: 0 });

// Up to 65535 channels per connection

Closing a Channel

dc.close();
// dc.readyState becomes 'closing' → 'closed'
// Remote side gets dc.onclose

Common Data Channel Gotchas

  • Create channels before createOffer — channels added after the offer require renegotiation.
  • send() on a non-open channel throws — always check dc.readyState === 'open' first.
  • Max message size varies by browser — Firefox has a ~256 KB limit per message; chunk large payloads to stay under 64 KB.
  • bufferedAmount grows unbounded — unguarded send() loops will queue everything in memory; use bufferedAmountLowThreshold.
  • ondatachannel only fires on the peer that did NOT call createDataChannel — the creator attaches handlers directly to dc.
  • Ordered + unreliable is not TCP — combining ordered: true with maxRetransmits: 0 can cause head-of-line blocking even without retransmits; use ordered: false for true UDP-like behavior.