Intermediate
20 min
Full Guide
WebRTC Data Channels
Send arbitrary data peer-to-peer with low latency using RTCDataChannel
What are Data Channels?
RTCDataChannel enables peer-to-peer transfer of arbitrary data—text, binary, files, game state—directly between browsers. Unlike WebSocket which requires a server, data channels let you send data with ultra-low latency directly to another user.
Data channels are perfect for multiplayer games, file sharing, collaborative tools, and any application where you need fast, direct communication between users.
🎮 Data Channel Use Cases
Gaming
- • Player position/state sync
- • Real-time input transmission
- • Game events and actions
File Transfer
- • Peer-to-peer file sharing
- • No server upload needed
- • Large file support with chunking
Collaboration
- • Cursor positions
- • Document edits (CRDT sync)
- • Drawing/whiteboard strokes
Chat
- • Text messages
- • Typing indicators
- • Read receipts
Creating Data Channels
// Data channels are created through RTCPeerConnection
const pc = new RTCPeerConnection(config);
// Create a data channel (initiating peer)
const channel = pc.createDataChannel('messages', {
ordered: true, // Guarantee order (like TCP)
maxRetransmits: 3, // Max resend attempts
// OR
maxPacketLifeTime: 3000, // Max ms to retransmit
protocol: '', // Sub-protocol (optional)
negotiated: false, // Auto-negotiate (default)
id: undefined // Channel ID (auto if not set)
});
// Channel events
channel.onopen = () => {
console.log('Data channel opened!');
channel.send('Hello, peer!');
};
channel.onmessage = (event) => {
console.log('Received:', event.data);
};
channel.onerror = (error) => {
console.error('Channel error:', error);
};
channel.onclose = () => {
console.log('Data channel closed');
};
// Receiving peer gets channel via ondatachannel
pc.ondatachannel = (event) => {
const receivedChannel = event.channel;
console.log('Received channel:', receivedChannel.label);
receivedChannel.onmessage = (e) => {
console.log('Message:', e.data);
};
};
// Check channel state
console.log(channel.readyState);
// 'connecting' | 'open' | 'closing' | 'closed'
console.log(channel.bufferedAmount); // Bytes queued to send
Channel Configuration Options
// Ordered vs Unordered delivery
// ORDERED (default) - Messages arrive in order sent
// Best for: Chat, file transfer, transactions
const orderedChannel = pc.createDataChannel('chat', {
ordered: true
});
// UNORDERED - Messages may arrive out of order
// Best for: Games, real-time position updates
const unorderedChannel = pc.createDataChannel('game-state', {
ordered: false
});
// Reliability options (can't use both together)
// RELIABLE (default) - Retransmit until delivered
const reliableChannel = pc.createDataChannel('files', {
ordered: true
// No maxRetransmits or maxPacketLifeTime = reliable
});
// PARTIALLY RELIABLE - Limited retransmissions
const partialChannel = pc.createDataChannel('positions', {
ordered: false,
maxRetransmits: 0 // Send once, no retransmit (UDP-like)
});
// TIME-LIMITED - Retransmit for N milliseconds
const timedChannel = pc.createDataChannel('voice-commands', {
ordered: false,
maxPacketLifeTime: 500 // Give up after 500ms
});
// Common configurations:
// For chat/messaging (reliable, ordered)
const chatChannel = pc.createDataChannel('chat', {
ordered: true
});
// For game state updates (fast, unordered)
const gameChannel = pc.createDataChannel('game', {
ordered: false,
maxRetransmits: 0
});
// For file transfer (reliable, ordered)
const fileChannel = pc.createDataChannel('files', {
ordered: true
});
// For cursor positions (partially reliable)
const cursorChannel = pc.createDataChannel('cursors', {
ordered: false,
maxRetransmits: 1
});
Sending Different Data Types
// Data channels support text and binary data
// STRING
channel.send('Hello, World!');
channel.send(JSON.stringify({ type: 'chat', message: 'Hi!' }));
// ARRAYBUFFER
const buffer = new ArrayBuffer(256);
const view = new Uint8Array(buffer);
view[0] = 42;
channel.send(buffer);
// TYPED ARRAYS
const floats = new Float32Array([1.0, 2.5, 3.14]);
channel.send(floats);
// BLOB
const blob = new Blob(['file contents'], { type: 'text/plain' });
channel.send(blob);
// Set binary type for receiving
channel.binaryType = 'arraybuffer'; // or 'blob'
channel.onmessage = (event) => {
if (typeof event.data === 'string') {
const message = JSON.parse(event.data);
handleTextMessage(message);
} else if (event.data instanceof ArrayBuffer) {
handleBinaryData(event.data);
} else if (event.data instanceof Blob) {
handleBlob(event.data);
}
};
// Efficient binary protocol for games
function sendPlayerState(x, y, rotation, health) {
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setFloat32(0, x);
view.setFloat32(4, y);
view.setFloat32(8, rotation);
view.setUint32(12, health);
channel.send(buffer);
}
function receivePlayerState(buffer) {
const view = new DataView(buffer);
return {
x: view.getFloat32(0),
y: view.getFloat32(4),
rotation: view.getFloat32(8),
health: view.getUint32(12)
};
}
File Transfer with Data Channels
// Complete peer-to-peer file transfer
class P2PFileTransfer {
constructor(dataChannel) {
this.channel = dataChannel;
this.channel.binaryType = 'arraybuffer';
this.receivingFile = null;
this.receivedChunks = [];
this.chunkSize = 64 * 1024; // 64KB chunks
this.channel.onmessage = (e) => this.handleMessage(e);
// Callbacks
this.onProgress = () => {};
this.onFileReceived = () => {};
}
async sendFile(file) {
// Send metadata first
this.channel.send(JSON.stringify({
type: 'file-start',
name: file.name,
size: file.size,
mimeType: file.type
}));
// Read and send in chunks
const buffer = await file.arrayBuffer();
const totalChunks = Math.ceil(buffer.byteLength / this.chunkSize);
for (let i = 0; i < totalChunks; i++) {
// Wait if buffer is full (flow control)
while (this.channel.bufferedAmount > 1024 * 1024) {
await new Promise(r => setTimeout(r, 50));
}
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, buffer.byteLength);
const chunk = buffer.slice(start, end);
this.channel.send(chunk);
this.onProgress(i + 1, totalChunks, 'sending');
}
// Send completion marker
this.channel.send(JSON.stringify({ type: 'file-end' }));
}
handleMessage(event) {
if (typeof event.data === 'string') {
const msg = JSON.parse(event.data);
if (msg.type === 'file-start') {
this.receivingFile = {
name: msg.name,
size: msg.size,
mimeType: msg.mimeType
};
this.receivedChunks = [];
} else if (msg.type === 'file-end') {
this.assembleFile();
}
} else {
// Binary chunk
this.receivedChunks.push(event.data);
const received = this.receivedChunks.reduce((a, b) => a + b.byteLength, 0);
const total = this.receivingFile?.size || 0;
this.onProgress(received, total, 'receiving');
}
}
assembleFile() {
const blob = new Blob(this.receivedChunks, {
type: this.receivingFile.mimeType
});
// Create download link
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = this.receivingFile.name;
a.click();
URL.revokeObjectURL(url);
this.onFileReceived(this.receivingFile.name, blob);
this.receivingFile = null;
this.receivedChunks = [];
}
}
// Usage
const fileTransfer = new P2PFileTransfer(dataChannel);
fileTransfer.onProgress = (current, total, direction) => {
const percent = Math.round((current / total) * 100);
console.log(`${direction}: ${percent}%`);
};
fileTransfer.onFileReceived = (name, blob) => {
console.log(`Received: ${name} (${blob.size} bytes)`);
};
// Send a file
const fileInput = document.getElementById('fileInput');
fileInput.onchange = () => {
fileTransfer.sendFile(fileInput.files[0]);
};
Multiplayer Game Example
// Real-time game state sync over data channel
class MultiplayerSync {
constructor(pc) {
this.pc = pc;
this.players = new Map();
this.localPlayerId = null;
// Create channels for different purposes
this.stateChannel = pc.createDataChannel('game-state', {
ordered: false,
maxRetransmits: 0 // Fast, unreliable
});
this.eventChannel = pc.createDataChannel('game-events', {
ordered: true // Reliable for important events
});
this.setupChannels();
}
setupChannels() {
this.stateChannel.onopen = () => {
console.log('State channel ready');
this.startStateSync();
};
this.stateChannel.onmessage = (e) => {
const state = this.decodeState(e.data);
this.updateRemotePlayer(state);
};
this.eventChannel.onmessage = (e) => {
const event = JSON.parse(e.data);
this.handleGameEvent(event);
};
}
startStateSync() {
// Send local player state 60 times per second
setInterval(() => {
if (this.stateChannel.readyState === 'open') {
const state = this.getLocalPlayerState();
this.stateChannel.send(this.encodeState(state));
}
}, 1000 / 60);
}
// Efficient binary encoding for position updates
encodeState(state) {
const buffer = new ArrayBuffer(28);
const view = new DataView(buffer);
view.setUint32(0, state.playerId);
view.setFloat32(4, state.x);
view.setFloat32(8, state.y);
view.setFloat32(12, state.z);
view.setFloat32(16, state.rotation);
view.setUint32(20, state.timestamp);
view.setUint8(24, state.animation);
view.setUint8(25, state.health);
return buffer;
}
decodeState(buffer) {
const view = new DataView(buffer);
return {
playerId: view.getUint32(0),
x: view.getFloat32(4),
y: view.getFloat32(8),
z: view.getFloat32(12),
rotation: view.getFloat32(16),
timestamp: view.getUint32(20),
animation: view.getUint8(24),
health: view.getUint8(25)
};
}
updateRemotePlayer(state) {
// Apply interpolation/prediction for smooth movement
let player = this.players.get(state.playerId);
if (!player) {
player = this.createPlayer(state.playerId);
this.players.set(state.playerId, player);
}
// Interpolate position
player.targetX = state.x;
player.targetY = state.y;
player.targetZ = state.z;
player.targetRotation = state.rotation;
player.health = state.health;
player.lastUpdate = Date.now();
}
// Reliable events (damage, pickups, etc.)
sendGameEvent(event) {
if (this.eventChannel.readyState === 'open') {
this.eventChannel.send(JSON.stringify(event));
}
}
handleGameEvent(event) {
switch (event.type) {
case 'damage':
this.applyDamage(event.targetId, event.amount);
break;
case 'pickup':
this.handlePickup(event.itemId, event.playerId);
break;
case 'spawn':
this.spawnPlayer(event.playerId, event.position);
break;
}
}
}
// Usage
const sync = new MultiplayerSync(peerConnection);
sync.localPlayerId = myPlayerId;
// In game loop
function gameLoop() {
// Interpolate remote players
sync.players.forEach(player => {
player.x += (player.targetX - player.x) * 0.1;
player.y += (player.targetY - player.y) * 0.1;
renderPlayer(player);
});
requestAnimationFrame(gameLoop);
}
// Send events
sync.sendGameEvent({
type: 'damage',
targetId: enemyId,
amount: 25
});
Flow Control and Backpressure
// Handle bufferedAmount to avoid overwhelming the channel
async function sendWithFlowControl(channel, data) {
// Check if buffer is getting full
const maxBuffer = 1024 * 1024; // 1MB threshold
if (channel.bufferedAmount > maxBuffer) {
// Wait for buffer to drain
await new Promise(resolve => {
channel.bufferedAmountLowThreshold = maxBuffer / 2;
channel.onbufferedamountlow = resolve;
});
}
channel.send(data);
}
// Throttle high-frequency updates
class ThrottledChannel {
constructor(channel, minInterval = 16) { // ~60fps
this.channel = channel;
this.minInterval = minInterval;
this.lastSend = 0;
this.pendingData = null;
this.timeout = null;
}
send(data) {
const now = Date.now();
const elapsed = now - this.lastSend;
if (elapsed >= this.minInterval) {
this.doSend(data);
} else {
// Store latest and schedule
this.pendingData = data;
if (!this.timeout) {
this.timeout = setTimeout(() => {
this.doSend(this.pendingData);
this.pendingData = null;
this.timeout = null;
}, this.minInterval - elapsed);
}
}
}
doSend(data) {
if (this.channel.readyState === 'open') {
this.channel.send(data);
this.lastSend = Date.now();
}
}
}
// Usage
const throttled = new ThrottledChannel(positionChannel, 16);
// Called on every mouse move - throttled to 60fps
document.onmousemove = (e) => {
throttled.send(JSON.stringify({ x: e.clientX, y: e.clientY }));
};
💡 Data Channel Best Practices
- ✓ Use unordered for real-time - Games and cursors don't need ordering
- ✓ Use binary for efficiency - DataView for structured binary data
- ✓ Monitor bufferedAmount - Implement flow control
- ✓ Chunk large files - 16-64KB chunks work well
- ✓ Separate channels - Different channels for different data types
- ✓ Handle channel close - Graceful degradation