WebSockets & Real-Time Apps
Broadcasting and Messaging Patterns
Broadcasting and Messaging Patterns
Socket.io provides multiple ways to send messages to different groups of clients. Understanding these broadcasting patterns and when to use each one is essential for building efficient real-time applications.
Basic Emit Patterns
Here are the fundamental ways to emit events in Socket.io:
// SERVER
io.on('connection', (socket) => {
// 1. Send to THIS socket only
socket.emit('message', 'Only you receive this');
// 2. Send to ALL sockets (including sender)
io.emit('message', 'Everyone receives this');
// 3. Send to ALL sockets EXCEPT sender
socket.broadcast.emit('message', 'Everyone except sender');
// 4. Send to specific room
io.to('roomName').emit('message', 'Only room members');
// 5. Send to room EXCEPT sender
socket.to('roomName').emit('message', 'Room members except sender');
// 6. Send to multiple rooms
io.to('room1').to('room2').emit('message', 'Multiple rooms');
// 7. Send to socket by ID
io.to(socketId).emit('message', 'Specific socket by ID');
});
Note:
socket.emit() sends to one client, io.emit() sends to all clients, and socket.broadcast.emit() sends to all except the sender.
Broadcasting to All Clients
Send a message to every connected client:
// SERVER
io.on('connection', (socket) => {
// Broadcast server announcement to everyone
io.emit('announcement', {
type: 'info',
message: 'Server maintenance in 5 minutes',
timestamp: new Date()
});
// Update user count for everyone
const updateUserCount = () => {
io.emit('userCount', io.engine.clientsCount);
};
updateUserCount(); // Send when someone connects
socket.on('disconnect', () => {
updateUserCount(); // Send when someone disconnects
});
});
// CLIENT
socket.on('announcement', (data) => {
console.log(`[${data.type}] ${data.message}`);
alert(data.message);
});
socket.on('userCount', (count) => {
document.getElementById('user-count').textContent = count;
});
Broadcasting to All Except Sender
Notify other users without notifying yourself:
// SERVER
io.on('connection', (socket) => {
// Notify others when user joins
socket.broadcast.emit('userJoined', {
userId: socket.id,
timestamp: new Date()
});
// Typing indicator
socket.on('typing', (isTyping) => {
socket.broadcast.emit('userTyping', {
userId: socket.id,
isTyping: isTyping
});
});
// User action notification
socket.on('userAction', (action) => {
socket.broadcast.emit('notification', {
userId: socket.id,
action: action,
timestamp: new Date()
});
});
});
// CLIENT
socket.on('userJoined', (data) => {
console.log(`User ${data.userId} joined`);
});
socket.on('userTyping', (data) => {
if (data.isTyping) {
showTypingIndicator(data.userId);
} else {
hideTypingIndicator(data.userId);
}
});
Broadcasting to Specific Rooms
Send messages to users in a specific room or multiple rooms:
// SERVER
io.on('connection', (socket) => {
// Join a room
socket.on('joinRoom', (roomName) => {
socket.join(roomName);
// Send to THIS user
socket.emit('joinedRoom', roomName);
// Send to ALL in room (including sender)
io.to(roomName).emit('roomUpdate', {
message: `User joined ${roomName}`,
userCount: io.sockets.adapter.rooms.get(roomName)?.size || 0
});
// Send to ALL in room EXCEPT sender
socket.to(roomName).emit('notification', {
message: 'New user joined the room'
});
});
// Send message to specific room
socket.on('roomMessage', (roomName, message) => {
// Broadcast to everyone in the room (including sender)
io.to(roomName).emit('roomMessage', {
userId: socket.id,
room: roomName,
message: message,
timestamp: new Date()
});
});
// Send to multiple rooms
socket.on('multiRoomMessage', (rooms, message) => {
let emitter = io;
rooms.forEach(room => {
emitter = emitter.to(room);
});
emitter.emit('message', message);
});
});
Private Messaging (Direct Messages)
Send a message to a specific user by their socket ID:
// SERVER
const users = new Map(); // Track socket ID to username mapping
io.on('connection', (socket) => {
// Register user
socket.on('register', (username) => {
users.set(socket.id, username);
socket.username = username;
});
// Private message by socket ID
socket.on('privateMessage', (targetSocketId, message) => {
// Send to recipient
io.to(targetSocketId).emit('privateMessage', {
from: socket.id,
fromUsername: socket.username,
message: message,
timestamp: new Date()
});
// Send confirmation to sender
socket.emit('messageSent', {
to: targetSocketId,
message: message,
timestamp: new Date()
});
});
// Private message by username
socket.on('sendToUser', (username, message) => {
// Find socket ID by username
let targetSocketId = null;
for (const [socketId, user] of users.entries()) {
if (user === username) {
targetSocketId = socketId;
break;
}
}
if (targetSocketId) {
io.to(targetSocketId).emit('privateMessage', {
from: socket.id,
fromUsername: socket.username,
message: message
});
} else {
socket.emit('error', `User ${username} not found`);
}
});
socket.on('disconnect', () => {
users.delete(socket.id);
});
});
// CLIENT
socket.emit('register', 'John');
// Send private message
socket.emit('privateMessage', 'target-socket-id', 'Hello privately!');
// Receive private message
socket.on('privateMessage', (data) => {
console.log(`Private message from ${data.fromUsername}: ${data.message}`);
});
Emit Cheat Sheet
Quick reference for all emit patterns:
// === BASIC PATTERNS ===
socket.emit('event', data) // → to sender only
io.emit('event', data) // → to all clients
socket.broadcast.emit('event', data) // → to all except sender
// === ROOM PATTERNS ===
io.to('room').emit('event', data) // → to all in room
socket.to('room').emit('event', data) // → to room except sender
io.in('room').emit('event', data) // → to all in room (same as .to())
// === MULTIPLE ROOMS ===
io.to('room1').to('room2').emit('event', data) // → to room1 AND room2
// === PRIVATE MESSAGE ===
io.to(socketId).emit('event', data) // → to specific socket
// === NAMESPACE ===
io.of('/namespace').emit('event', data) // → to all in namespace
// === VOLATILE (may be lost) ===
socket.volatile.emit('event', data) // → may be dropped if not ready
// === WITH ACKNOWLEDGEMENT ===
socket.emit('event', data, (response) => {}) // → with callback
Tip: Save this cheat sheet as a comment in your code. It's easy to forget which emit pattern to use in different scenarios!
Message Acknowledgement Patterns
Request confirmation that a message was received:
// SERVER
io.on('connection', (socket) => {
// Pattern 1: Client requests, server acknowledges
socket.on('sendMessage', (message, callback) => {
// Validate message
if (!message || message.length === 0) {
callback({ success: false, error: 'Empty message' });
return;
}
// Broadcast message
io.emit('message', {
from: socket.id,
message: message,
timestamp: new Date()
});
// Acknowledge success
callback({ success: true, messageId: Date.now() });
});
// Pattern 2: Server requests, client acknowledges
socket.emit('serverQuestion', 'Are you still there?', (response) => {
console.log('Client responded:', response);
});
// Pattern 3: Timeout for acknowledgement
socket.timeout(5000).emit('requestData', (err, response) => {
if (err) {
console.log('Client did not respond in time');
} else {
console.log('Client responded:', response);
}
});
});
// CLIENT
// Respond to pattern 1
socket.emit('sendMessage', 'Hello!', (response) => {
if (response.success) {
console.log('Message sent, ID:', response.messageId);
} else {
console.error('Failed:', response.error);
}
});
// Respond to pattern 2
socket.on('serverQuestion', (question, callback) => {
console.log('Server asked:', question);
callback('Yes, I am here!');
});
Broadcast Flags and Modifiers
Control broadcasting behavior with flags:
// SERVER
io.on('connection', (socket) => {
// Volatile: May be lost if client not ready (good for high-frequency updates)
socket.volatile.emit('cursorPosition', { x: 100, y: 200 });
// Broadcast volatile
socket.volatile.broadcast.emit('gameState', gameData);
// Binary: Optimize for binary data
socket.binary(true).emit('image', buffer);
// Compress: Enable per-message compression (Socket.io 4.0+)
socket.compress(true).emit('largeData', bigObject);
// Local: Emit only on this server (not to other servers in cluster)
socket.local.emit('localEvent', data);
// Combine flags
socket.volatile.compress(true).to('room').emit('optimizedData', data);
});
Complete Broadcasting Example
// SERVER - Complete chat application
const io = require('socket.io')(3000);
const users = new Map();
const rooms = new Map();
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
// Register user
socket.on('register', (username) => {
users.set(socket.id, username);
socket.username = username;
// Notify everyone
io.emit('userJoined', {
userId: socket.id,
username: username,
totalUsers: users.size
});
});
// Join room
socket.on('joinRoom', (roomName) => {
socket.join(roomName);
// Track room membership
if (!rooms.has(roomName)) {
rooms.set(roomName, new Set());
}
rooms.get(roomName).add(socket.id);
// Confirm to user
socket.emit('roomJoined', {
room: roomName,
userCount: rooms.get(roomName).size
});
// Notify room members (except sender)
socket.to(roomName).emit('userJoinedRoom', {
username: socket.username,
room: roomName
});
});
// Public room message
socket.on('roomMessage', (roomName, message) => {
io.to(roomName).emit('roomMessage', {
from: socket.id,
username: socket.username,
room: roomName,
message: message,
timestamp: new Date()
});
});
// Private message
socket.on('privateMessage', (targetUsername, message) => {
// Find target socket
let targetSocketId = null;
for (const [socketId, username] of users.entries()) {
if (username === targetUsername) {
targetSocketId = socketId;
break;
}
}
if (targetSocketId) {
// Send to recipient
io.to(targetSocketId).emit('privateMessage', {
from: socket.id,
fromUsername: socket.username,
message: message,
timestamp: new Date()
});
// Confirm to sender
socket.emit('privateMessageSent', {
to: targetUsername,
message: message
});
} else {
socket.emit('error', `User ${targetUsername} not found`);
}
});
// Typing indicator (volatile, room-specific)
socket.on('typing', (roomName, isTyping) => {
socket.volatile.to(roomName).emit('userTyping', {
userId: socket.id,
username: socket.username,
isTyping: isTyping
});
});
// Global announcement (admin only)
socket.on('announcement', (message, adminKey) => {
if (adminKey === 'secret-admin-key') {
io.emit('announcement', {
message: message,
timestamp: new Date()
});
}
});
// Disconnect
socket.on('disconnect', () => {
const username = users.get(socket.id);
users.delete(socket.id);
// Remove from room tracking
rooms.forEach((members, roomName) => {
if (members.has(socket.id)) {
members.delete(socket.id);
io.to(roomName).emit('userLeftRoom', {
username: username,
room: roomName
});
}
});
// Notify everyone
io.emit('userLeft', {
userId: socket.id,
username: username,
totalUsers: users.size
});
});
});
console.log('Chat server running on port 3000');
Broadcasting Best Practices
// ✅ GOOD - Use appropriate patterns
socket.emit('welcome', 'Hello!'); // Send to this user
socket.broadcast.emit('newUser', userId); // Notify others
// ✅ GOOD - Use volatile for high-frequency updates
socket.volatile.emit('cursor', { x, y }); // OK to lose some updates
// ✅ GOOD - Use rooms for targeted broadcasting
io.to('game123').emit('gameUpdate', data); // Only relevant users
// ❌ BAD - Don't send to everyone when you should use rooms
io.emit('gameUpdate', data); // Everyone gets it!
// ❌ BAD - Don't send large data frequently
io.emit('heavyData', largeObject); // Slows down server
// ✅ GOOD - Throttle or compress large data
socket.compress(true).emit('data', large); // Compress first
Exercise: Build a comprehensive messaging system:
- Implement public broadcast messages (all users)
- Implement room-based broadcasting (chat channels)
- Implement private messaging between users (direct messages)
- Add message acknowledgements with success/error callbacks
- Implement volatile events for typing indicators
- Add a command to broadcast to multiple rooms simultaneously
- Track and display online user counts per room
- Implement an admin command that broadcasts to all namespaces