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