Targeted Broadcasting with Socket.IO Rooms and Acknowledgements: A Practical Guide
Learn how to use Socket.IO rooms for targeted broadcasting and acknowledgements to guarantee message delivery to specific clients, with a step‑by‑step example and practical trade‑offs for scaling.
20 Jul 2025, 06:44 UTC

The Problem
In many real‑time apps you only want to send an update to a subset of users – for example, a private chat, a live game state, or a per‑user notification. Broadcasting to every connected socket with io.emit wastes bandwidth, increases latency, and can overwhelm clients that don’t need the data.
Rooms: Server‑Side Grouping
Socket.IO’s rooms are a lightweight server‑side construct that lets you route events to a named group of sockets. A socket joins a room with socket.join('roomName') and the server can emit to that room with io.to('roomName').emit('event', payload). Only sockets that have joined the room receive the event.
Key points:
- Rooms are not persisted across reconnects – a new socket id means the server must re‑run the join logic.
- They are not a message queue; they provide routing only.
- When you scale horizontally you must use an adapter (e.g., Redis) so room membership propagates across nodes.
Acknowledgements: Confirming Delivery
Socket.IO lets you pass a callback as the last argument of emit. The receiver calls this callback to acknowledge that it processed the event. The sender’s callback runs only after the receiver invokes it, giving you an application‑level “message delivered” signal.
Example:
// Server
socket.on('sendMessage', (msg, ack) => {
// Broadcast to a per‑user room
io.to(`user:${msg.userId}`).emit('newMessage', msg, (response) => {
// ack is called for each receiver that invokes it
console.log('Receiver acknowledged:', response);
});
});
// Client
socket.emit('sendMessage', { userId: 42, text: 'Hello' }, (res) => {
console.log('Server received ack:', res);
});
When a room contains many sockets, a single callback cannot represent all receivers. Acknowledgements work best with single‑target rooms (e.g., a per‑user room) or direct socket.emit calls.
Scaling Across Nodes
With the default in‑memory adapter, rooms exist only on one Node.js process. If you run multiple instances behind a load balancer, a broadcast to a room will reach only sockets connected to that instance. The Redis adapter solves this by synchronising room membership and broadcasts across all nodes.
Configuration example:
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const io = new Server();
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
Practical Example: Per‑User Room with Acknowledgement
Below is a minimal server and client pair that demonstrates:
- Joining a per‑user room on connection.
- Sending a message to that room.
- Using an acknowledgement to confirm receipt.
Server (Node.js)
const { Server } = require('socket.io');
const io = new Server(3000);
io.on('connection', (socket) => {
const userId = socket.handshake.auth.userId;
if (!userId) return;
socket.join(`user:${userId}`);
console.log(`Socket ${socket.id} joined room user:${userId}`);
socket.on('chatMessage', (msg, ack) => {
// Broadcast only to the target user’s room
io.to(`user:${msg.recipientId}`).emit('chatMessage', msg, (response) => {
console.log(`Ack from receiver:`, response);
if (ack) ack(response);
});
});
});
Client (Browser)
const socket = io('http://localhost:3000', {
auth: { userId: 42 }
});
socket.on('chatMessage', (msg) => {
console.log('Received message:', msg);
// Acknowledge back to the server
return { status: 'delivered', messageId: msg.id };
});
// Send a message to another user
socket.emit('chatMessage', { recipientId: 99, id: 123, text: 'Hi!' }, (ack) => {
console.log('Server ack:', ack);
});
Trade‑offs and Limitations
- No Delivery Guarantee: Rooms only route; if a socket disconnects before receiving, the message is lost.
- Ordering Not Guaranteed across reconnections or different nodes.
- Scalability Cost: Even with rooms, a broadcast still sends one packet per socket in the room.
- Adapter Overhead: Using Redis adds network round‑trips; measure latency if you need ultra‑low delay.
How to Verify
- Check the Socket.IO version with
npm list socket.io socket.io-clientand read the migration notes for your major version. - Run the example locally with two browser tabs: one joined to
user:42, another touser:99. Send a message from the first and confirm only the second receives it. - Test acknowledgements by inspecting console logs for the ack callbacks.
- For multi‑node setups, start two server instances on different ports, connect clients to each, and observe that without Redis only the local instance receives the room broadcast. Add the Redis adapter and repeat the test.
Takeaway
Using Socket.IO rooms together with acknowledgements gives you a clean, server‑side way to target specific clients and confirm that they processed the event. It keeps traffic low, improves UX, and scales cleanly when combined with a Redis adapter. Just remember that rooms are routing, not queuing, and acknowledgements work best when the target set is small or single‑socket.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.