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
maxPacketLifetimeormaxRetransmits, 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
| Option | Type | Default | Description |
|---|---|---|---|
ordered | boolean | true | Guarantee in-order delivery |
maxPacketLifetime | number | null | ms before giving up on a message |
maxRetransmits | number | null | Retransmit attempts before dropping |
protocol | string | "" | Application-level sub-protocol name |
negotiated | boolean | false | If true, both sides create the channel manually |
id | number | auto | Stream ID for negotiated channels |
Reliability Modes
| Mode | ordered | maxRetransmits | maxPacketLifetime | Like |
|---|---|---|---|---|
| Reliable ordered (default) | true | none | none | TCP |
| Reliable unordered | false | none | none | TCP but unordered |
| Unreliable ordered | true | 0 | — | — |
| Unreliable unordered | false | 0 | — | UDP |
| Partially reliable | either | N | — | SCTP 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()); } };
| Property | Type | Description |
|---|---|---|
dc.bufferedAmount | number | Bytes queued but not yet sent |
dc.bufferedAmountLowThreshold | number | onbufferedamountlow 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 checkdc.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.
bufferedAmountgrows unbounded — unguardedsend()loops will queue everything in memory; usebufferedAmountLowThreshold.ondatachannelonly fires on the peer that did NOT callcreateDataChannel— the creator attaches handlers directly todc.- Ordered + unreliable is not TCP — combining
ordered: truewithmaxRetransmits: 0can cause head-of-line blocking even without retransmits; useordered: falsefor true UDP-like behavior.