WebSockets & Real-Time Apps

Broadcasting and Messaging Patterns

20 min Lesson 10 of 35

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