Socket.IO Rooms Stay Local Until You Add an Adapter
Socket.IO rooms broadcast only within one Node process. Learn when to add the Redis adapter and verify cross-node delivery.
13 May 2026, 15:58 UTC

The broadcast that disappears after you scale
You run a Node.js server with Socket.IO. Clients connect, join a room such as chat:room-42, and your server emits to that room with io.to('chat:room-42').emit('message', payload). It works on a single server.
Then you add a second Node replica behind a load balancer. Two users join the same room, but one lands on Node 1 and the other on Node 2. When Node 1 emits to the room, only clients connected to Node 1 receive it. The user on Node 2 hears nothing.
This is not a bug. Socket.IO rooms live in memory on the server instance where the sockets are connected. A room is not a cluster-wide topic unless you configure an adapter to share membership and events between processes.
What a room is, and what it is not
A Socket.IO room is a named group on a single server instance. A socket joins with socket.join('room-name'), and the server tracks the membership locally. You can broadcast to every socket in that room without maintaining your own client list.
Rooms are not namespaces. Namespaces create separate connection endpoints, such as /chat and /game. Rooms are logical groups inside a namespace. Rooms are also not a security boundary: any socket that knows the room name can call socket.join(), so authorize membership in your connection or join handler if the room carries sensitive data.
Scale options: sticky sessions or an adapter
If you run multiple Node processes, you have two practical choices:
- Sticky sessions. Configure the load balancer so a client always reaches the same Node process. Room broadcasts work because all members of a room stay on one node. The downside is uneven load, and a node failure disconnects its clients.
- Socket.IO adapter. Add an adapter such as the official Redis adapter for Socket.IO v4 (
@socket.io/redis-adapter). It publishes room events over Redis pub/sub (publish-subscribe, a pattern where messages go to a channel and interested subscribers receive them), so every instance knows which sockets belong to a room and can forward broadcasts. This works with non-sticky load balancing.
Choose sticky sessions only for a short-term fix or when you cannot run Redis. Choose the adapter before you deploy multiple replicas if you need reliable room broadcasts.
Worked example: Redis adapter with Socket.IO v4
Assumptions: Socket.IO v4.x, a Node.js project, and a Redis server reachable from your Node instances. Run these commands in your project directory. No special permissions are required beyond normal npm install access.
npm install socket.io@4 @socket.io/redis-adapter ioredis
Create the server:
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('ioredis');
const pubClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
const io = new Server(server, {
cors: { origin: '*' } // tighten this for production
});
pubClient.connect().then(() => {
subClient.connect().then(() => {
io.adapter(createAdapter(pubClient, subClient));
io.listen(3000);
});
});
io.on('connection', (socket) => {
socket.on('joinRoom', (room) => {
// TODO: authorize the user for this room before joining
socket.join(room);
});
socket.on('sendMessage', ({ room, message }) => {
io.to(room).emit('newMessage', message);
});
});
Placeholders: replace redis://localhost:6379 with your Redis URL. Tighten the cors origin before production.
Expected check: start two Node instances of this server behind a load balancer without sticky sessions. Connect Client A to instance 1 and Client B to instance 2. Have both join room-1. Emit sendMessage from Client A. With the adapter connected, Client B should receive newMessage. Without the adapter, Client B will not.
Risk: changing the adapter requires restarting every Node instance. Room membership is rebuilt from Redis, but in-flight messages during a restart can be lost.
Limitations and how to verify
- Room state is in memory by default. A process restart empties rooms unless clients rejoin or an adapter rebuilds membership.
- The Redis adapter shares room metadata over pub/sub; it does not persist message history.
- Verify your setup by checking
socket.roomsaftersocket.join(). For cross-node tests, confirm a message emitted on one node reaches a client connected to another node. - Check your installed major version with
npm list socket.iobefore adopting an adapter package, because setup changed between Socket.IO major versions.
Decide before you add a second node
Use rooms for chat channels, game lobbies, or notification groups on a single server without extra infrastructure. If you plan to run more than one Socket.IO process, add the Redis adapter—or commit to sticky sessions—before clients start missing messages. Test with two instances and two clients on different nodes; that single check will tell you whether your room broadcasts are truly cluster-wide.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.