Skip to content

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

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.

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)

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

  1. ****:video 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!)
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);
};

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

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

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

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

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

// 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
}));
}
}
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;
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

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.

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.

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.