Skip to content

WebSockets vs WebRTC: Signaling vs Peer-to-Peer Communication

Overview

Quick Summary

WebRTC and WebSockets serve different real-time communication needs: WebRTC enables peer-to-peer connections ideal for video, audio, and direct data transfer, while WebSockets excel at client-server communication for general real-time features. The two technologies are complementary, with WebSockets often handling WebRTC signaling.

At a Glance Comparison

FeatureWebSocketsWebRTC
ArchitectureClient-ServerPeer-to-Peer
Connection SetupSimple HTTP upgradeComplex ICE/STUN/TURN
Media Support❌ Data onlyβœ… Audio, Video, Data
NAT Traversal❌ Not neededβœ… Built-in
EncryptionOptional (WSS)Mandatory (DTLS/SRTP)
Server RequiredAlwaysOnly for signaling
Browser Support99%+95%
Use CasesChat, notifications, updatesVideo calls, file sharing, gaming
ComplexityLow-MediumHigh
Data ChannelsSingle streamMultiple streams
ProtocolTCPUDP (SCTP for data)

How WebRTC Works

WebRTC enables direct peer-to-peer communication between browsers:

WebRTC Components

  1. ****
    capture and streaming
  2. RTCPeerConnection: Peer connection management
  3. RTCDataChannel: Application data transfer
  4. ICE: NAT traversal and connectivity
  5. Signaling: Exchange of connection information (via WebSocket!)

WebRTC Implementation

class WebRTCPeer {
constructor(signaling) {
this.signaling = signaling; // Usually WebSocket
this.pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'user',
credential: 'pass'
}
]
});
this.setupPeerConnection();
}
setupPeerConnection() {
// Handle incoming media streams
this.pc.ontrack = (event) => {
const [remoteStream] = event.streams;
this.displayRemoteVideo(remoteStream);
};
// Handle ICE candidates
this.pc.onicecandidate = (event) => {
if (event.candidate) {
this.signaling.send({
type: 'ice-candidate',
candidate: event.candidate
});
}
};
// Handle connection state changes
this.pc.onconnectionstatechange = () => {
console.log('Connection state:', this.pc.connectionState);
};
}
async startCall(withVideo = true) {
// Get local media
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: withVideo
});
// Add local stream to peer connection
stream.getTracks().forEach(track => {
this.pc.addTrack(track, stream);
});
// Create and send offer
const offer = await this.pc.createOffer();
await this.pc.setLocalDescription(offer);
this.signaling.send({
type: 'offer',
sdp: offer
});
}
async handleOffer(offer) {
await this.pc.setRemoteDescription(offer);
// Get local media
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
stream.getTracks().forEach(track => {
this.pc.addTrack(track, stream);
});
// Create and send answer
const answer = await this.pc.createAnswer();
await this.pc.setLocalDescription(answer);
this.signaling.send({
type: 'answer',
sdp: answer
});
}
async handleAnswer(answer) {
await this.pc.setRemoteDescription(answer);
}
async handleIceCandidate(candidate) {
await this.pc.addIceCandidate(candidate);
}
}
// WebRTC peer connection with data channel
class WebRTCConnection {
constructor(signalingSocket) {
this.signalingSocket = signalingSocket;
this.peerConnection = null;
this.dataChannel = null;
this.rooms = new Map();
}
async initializeConnection(isInitiator) {
// Create peer connection
this.peerConnection = new RTCPeerConnection({
turnserver
// Handle ICE candidates
this.peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.signalingSocket.send(JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate
}));
}
};
if (isInitiator) {
// Create data channel
this.dataChannel = this.peerConnection.createDataChannel('data');
this.setupDataChannel();
// Create offer
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
this.signalingSocket.send(JSON.stringify({
type: 'offer',
offer: offer
}));
} else {
// Wait for data channel
this.peerConnection.ondatachannel = (event) => {
this.dataChannel = event.channel;
this.setupDataChannel();
};
}
setupDataChannel() {
this.dataChannel.onopen = () => {
console.log('Data channel opened');
this.dataChannel.send('Hello peer!');
};
this.dataChannel.onmessage = (event) => {
console.log('Received:', event.data);
};
this.dataChannel.onerror = (error) => {
console.error('Data channel error:', error);
};
}
async handleSignalingMessage(event) {
const data = JSON.parse(event.data);
switch(data.type) {
case 'offer':
await this.peerConnection.setRemoteDescription(data.offer);
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);
this.signalingSocket.send(JSON.stringify({
type: 'answer',
answer: answer
}));
break;
case 'answer':
await this.peerConnection.setRemoteDescription(data.answer);
break;
case 'ice-candidate':
await this.peerConnection.addIceCandidate(data.candidate);
break;
}
}
}
// WebSocket for signaling
const signalingSocket = new WebSocket('wss://signaling.example.com');
const rtcConnection = new WebRTCConnection(signalingSocket);
signalingSocket.onmessage = (event) => {
rtcConnection.handleSignalingMessage(event);
};

How WebSockets Work

WebSockets provide persistent client-server connections:

// WebSocket - Simple client-server communication
const ws = new WebSocket('wss://api.example.com/socket');
ws.onopen = () => {
console.log('Connected to server');
ws.send(JSON.stringify({
type: 'join',
room: 'lobby'
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleServerMessage(message);
};
// All communication goes through server
function broadcastToRoom(data) {
ws.send(JSON.stringify({
type: 'broadcast',
data: data
}));
##KeyDifferences### Connection Architecture
**WebSockets**: Client-Server Star Topology
- All clients connect to central server
- Server mediates all communication
- Simple connection management
- Server controls message routing
**WebRTC**: Peer-to-Peer Mesh
- Direct connections between peers
- No server needed for data transfer
- Complex connection establishment
- Peers communicate directly
### Connection Establishment
**WebSockets**: Simple upgrade
```javascript
// WebSocket - One step connection
const ws = new WebSocket('wss://example.com');
ws.onopen = () => console.log('Connected!');

WebRTC: Complex negotiation

// WebRTC - Multi-step process
// 1. Create peer connection
// 2. Exchange offers/answers via signaling
// 3. Exchange ICE candidates
// 4. Establish connection through NAT/firewall

Data Transfer Characteristics

WebSockets: TCP-based reliable delivery

  • Ordered message delivery
  • Automatic retransmission
  • Connection-oriented
  • Higher latency for reliability

WebRTC Data Channels: Flexible delivery

  • Reliable or unreliable modes
  • Ordered or unordered delivery
  • Message or stream oriented
  • Lower latency possible

Use Case Analysis

When WebRTC Excels

βœ… Media streaming:

  • Video conferencing
  • Voice calls
  • Screen sharing
  • Live broadcasting (peer-to-peer)

βœ… Direct data transfer:

  • File sharing between users
  • P2P content distribution
  • Collaborative editing (direct sync)
  • Peer-to-peer gaming

βœ… Privacy-sensitive applications:

  • End-to-end encrypted communication
  • No server intermediary for data
  • Direct peer connections
  • Reduced server costs

When WebSockets Excel

βœ… General real-time features:

  • Chat and messaging
  • Notifications
  • Live updates
  • Presence indicators

βœ… Server-mediated communication:

  • Broadcasting to many clients
  • Server-side processing needed
  • Centralized state management
  • Message persistence

βœ… Simple implementation needs:

  • Quick to implement
  • No NAT traversal complexity
  • Predictable connection model
  • Easier debugging

How They Work Together

WebRTC and WebSockets are often used together, with WebSockets handling signaling:

Typical Architecture

// Video calling application architecture
class VideoCallApp {
constructor() {
this.signaling = new WebSocket('wss://api.example.com/signal');
this.peerConnections = new Map();
this.setupSignaling();
}
setupSignaling() {
this.signaling.onmessage = async (event) => {
const message = JSON.parse(event.data);
switch(message.type) {
case 'user-joined':
// New user joined, prepare for potential call
this.prepareForUser(message.userId);
break;
case 'call-offer':
// Received WebRTC offer via WebSocket
await this.handleOffer(message.userId, message.offer);
break;
case 'call-answer':
// Received WebRTC answer via WebSocket
await this.handleAnswer(message.userId, message.answer);
break;
case 'ice-candidate':
// ICE candidate via WebSocket
await this.handleIceCandidate(message.userId, message.candidate);
break;
case 'chat-message':
// Regular chat via WebSocket (not video)
this.displayChatMessage(message);
break;
}
};
}
// Initiate call via WebSocket signaling
async startCall(userId) {
if (this.callState !== 'idle') return;
this.callState = 'calling';
// Request call setup via WebSocket
this.ws.send(JSON.stringify({
type: 'call_request',
targetUserId: userId
}));
// WebRTC connection will be established via signaling
}
handleUserJoined(message) {
const { userId } = message;
// Update UI via WebSocket info
this.updateParticipantsList(message.participants);
// Initialize WebRTC connection for media
this.webRTCManager.createPeerConnection(userId);
}
// Chat via WebSocket (server-mediated)
async startCall(userId) {
const pc = new RTCPeerConnection(this.iceConfig);
this.peerConnections.set(userId, pc);
// Add local stream
this.localStream.getTracks().forEach(track => {
pc.addTrack(track, this.localStream);
});
// Create offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Send offer via WebSocket signaling
this.signaling.send(JSON.stringify({
type: 'call-offer',
userId: userId,
offer: offer
}));
}
async createPeerConnection(userId) {
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
this.peerConnections.set(userId, pc);
// Get local media
if (!this.localStream) {
this.localStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
}
// Add local stream tracks
this.localStream.getTracks().forEach(track => {
pc.addTrack(track, this.localStream);
});
return pc;
}
sendChatMessage(text) {
// Chat messages go through WebSocket
this.signaling.send(JSON.stringify({
type: 'chat-message',
text: text
}));
}
}

Implementation Examples

Complete Multi-User Video Conference

class VideoChat {
constructor(signalingUrl, config = {}) {
this.signalingUrl = signalingUrl;
this.config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
...config.iceServers || []
],
...config
// WebSocket signaling server
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const rooms = new Map();
wss.on('connection', (ws) => {
let currentRoom = null;
let userId = null;
on('message',message const data = JSON.parse(message);
switch(data.type) {
case 'join-room':
currentRoom = data.room;
userId = data.userId;
// Add to room
if (!rooms.has(currentRoom)) {
rooms.set(currentRoom, new Set());
}
rooms.get(currentRoom).add(ws);
// Notify others in room
broadcast(currentRoom, {
type: 'user-joined',
userId: userId
}, ws);
break;
case 'signal':
// Forward WebRTC signaling to specific user
const targetWs = findUserWebSocket(data.targetUserId);
if (targetWs) {
targetWs.send(JSON.stringify({
type: 'signal',
signal: data.signal,
fromUserId: userId
}));
}
break;
case 'chat':
// Broadcast chat to room via WebSocket
broadcast(currentRoom, {
type: 'chat',
message: data.message,
userId: userId
); ws.on('close', () => {
if (currentRoom && rooms.has(currentRoom)) {
rooms.get(currentRoom).delete(ws);
broadcast(currentRoom, {
type: 'user-left',
userId: userId
});
});
function broadcast(room, message, exclude) {
if (rooms.has(room)) {
rooms.get(room).forEach(client => {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
(WebSocket + WebRTC)// Client using both WebSocket and WebRTC
class ConferenceClient {
constructor(roomId, userId) {
this.roomId = roomId;
this.userId = userId;
this.peers = new Map();
// WebSocket for signaling and chat
this.ws = new WebSocket('wss://signal.example.com');
// Local media stream
this.localStream = null;
// Application state
this.roomId = nullinitialize();
}
async initialize() {
await this.setupSignaling();
await this.setupLocalMedia();
this.setupUI();
}
async setupSignaling() {
this.signaling = new WebSocket(this.signalingUrl);
this.signaling.onopen = () => {
console.log('Signaling connected');
this.authenticate();
async initialize() {
// Get local media
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
// Set up WebSocket handlers
this.ws.onopen = () => {
this.ws.send(JSON.stringify({
type: 'join-room',
room: this.roomId,
userId: this.userId
}));
};
this.ws.onmessage = async (event) => {
const data = JSON.parse(event.data);
switch(data.type) {
case 'user-joined':
// Create WebRTC connection to new user
await this.createPeerConnection(data.userId, true);
break;
case 'signal':
// Handle WebRTC signaling
await this.handleSignal(data.fromUserId, data.signal);
break;
case 'chat':
// Display chat message (via WebSocket)
this.displayChat(data.userId, data.message);
break;
case 'user-left':
// Clean up WebRTC connection
this.removePeerConnection(data.userId);
break;
}
};
}
async createPeerConnection(peerId, createOffer) {
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// Add local stream
this.localStream.getTracks().forEach(track => {
pc.addTrack(track, this.localStream);
});
// Handle remote stream
pc.ontrack = (event) => {
this.displayRemoteStream(peerId, event.streams[0]);
};
// Handle ICE candidates
pc.onicecandidate = (event) => {
sendSignalpeerId, ice-candidatepeerssetpeerId, pc if (createOffer) {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
this.sendSignal(peerId, {
}
sendSignal(targetUserId, signal) {
this.ws.send(JSON.stringify({
type: 'signal',
targetUserId: targetUserId,
signal: signal
}));
}
sendChatMessage(message) {
// Chat goes through WebSocket, not WebRTC
this.ws.send(JSON.stringify({
type: 'chat',
message: message
}));
}
}
// Export for testing
module.exports = VideoChatServer;

Choosing the Right Technology

Decision Matrix

RequirementWebSocketWebRTCBoth
Text chatβœ… Betterβœ… PossibleBest
Video callingβŒβœ… Requiredβœ…
File sharingβœ… Worksβœ… Better for P2PDepends
Gaming (real-time)βœ… Goodβœ… Lower latencyβœ…
Notificationsβœ… Ideal❌ Overkill
Broadcastingβœ… Efficient❌ Not scalable
IoT devicesβœ… Simple❌ Too complex

Working with Real-Time Solutions

Rather than implementing raw protocols, consider comprehensive solutions:

WebSocket Platforms provide:

  • Managed infrastructure
  • Automatic scaling
  • Global presence
  • Connection reliability

WebRTC Platforms offer:

  • TURN server infrastructure
  • Signaling services
  • Media servers for recording
  • Bandwidth optimization Unified Platforms like Ably support:
  • WebSocket communication
  • WebRTC signaling
  • Hybrid architectures
  • Protocol abstraction

These solutions handle the complexity while providing reliable, scalable real-time features.

Conclusion

The relationship between WebSockets and WebRTC is fundamentally complementary. Each protocol excels in its domain and together they form the foundation of real-time communication. WebSockets excel at client-server communication for general real-time web features, while WebRTC enables peer-to-peer connections for media streaming and direct data transfer. Most video calling applications use both: WebRTC for media and WebSockets for signaling and chat.

Key Takeaways:

  1. WebRTC is for peer-to-peer, especially media streaming
  2. WebSockets are essential for client-server real-time communication
  3. They work together in modern video conferencing and collaboration apps
  4. WebSockets often handle WebRTC signaling
  5. Choose based on your architecture needs

For most real-time features, WebSockets provide the simpler, more reliable solution. Reserve WebRTC for when you specifically need peer-to-peer communication or media streaming.

Further Reading

While raw WebSocket or WebRTC implementation is complex, production applications often benefit from using established libraries like Socket.IO or commercial services that handle protocol complexities, connection management, and scaling challenges.


Written by Matthew O’Riordan, Co-founder & CEO of Ably, with experience building real-time systems reaching 2 billion+ devices monthly.