- Real-Time WebSocket Architecture Series: Part 1 – Understanding WebSocket Fundamentals
- Real-Time WebSocket Architecture Series: Part 2 – Building Your First WebSocket Server (Node.js)
- Real-Time WebSocket Architecture Series: Part 3 – Essential Features (Rooms, Namespaces & Events)
- Real-Time WebSocket Architecture Series: Part 4 – Authentication & Security
- Real-Time WebSocket Architecture Series: Part 5 – Scaling with Redis
- Real-Time WebSocket Architecture Series: Part 6 – Production-Grade Features
- Real-Time WebSocket Architecture Series: Part 7 – Serverless WebSocket Implementation with AWS Lambda
- Real-Time WebSocket Architecture Series: Part 8 – Edge Computing WebSockets with Ultra-Low Latency
Welcome to Part 3 of our Real-Time WebSocket Architecture series! In Part 2, we built our first WebSocket server with basic messaging. Now we’ll add powerful features that make Socket.io shine: Rooms, Namespaces, and advanced Event Handling.
These features are essential for building scalable, organized real-time applications like multi-room chat systems, gaming platforms, and collaborative tools.
Understanding the Architecture
Before diving into code, let’s understand how Socket.io organizes connections:
graph TB
    Server[Socket.io Server]
    
    Server --> NS1[Default Namespace '/']
    Server --> NS2[Namespace '/chat']
    Server --> NS3[Namespace '/admin']
    
    NS1 --> R1[All Sockets]
    
    NS2 --> R2[Room: general]
    NS2 --> R3[Room: javascript]
    NS2 --> R4[Room: python]
    
    NS3 --> R5[Room: moderators]
    
    R2 --> U1[User 1]
    R2 --> U2[User 2]
    
    R3 --> U3[User 3]
    R3 --> U4[User 4]
    
    R4 --> U5[User 5]
    
    style Server fill:#667eea,color:#fff
    style NS1 fill:#48bb78,color:#fff
    style NS2 fill:#48bb78,color:#fff
    style NS3 fill:#48bb78,color:#fff
    style R2 fill:#4299e1,color:#fff
    style R3 fill:#4299e1,color:#fff
    style R4 fill:#4299e1,color:#fffNamespaces: The Top-Level Segmentation
What Are Namespaces?
Namespaces are separate communication channels that allow you to split your application logic over a single shared connection. Think of them as different floors in a building – each floor has its own set of rooms and users.
Key characteristics:
- Each namespace has its own event handlers
- Sockets in one namespace cannot directly communicate with another
- Reduces overhead by multiplexing over a single WebSocket connection
- Perfect for separating concerns (e.g., chat, notifications, admin)
When to Use Namespaces
- Different application modules: Separate chat from notifications from admin panel
- Access control: Restrict certain features to authenticated users
- Multi-tenancy: Isolate different organizations or workspaces
- Feature isolation: Keep related functionality together
Creating Namespaces – Server Side
// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// Default namespace (always exists)
io.on('connection', (socket) => {
  console.log('User connected to default namespace');
});
// Chat namespace
const chatNamespace = io.of('/chat');
chatNamespace.on('connection', (socket) => {
  console.log(`User ${socket.id} connected to /chat`);
  
  socket.on('send-message', (data) => {
    chatNamespace.emit('receive-message', data);
  });
  
  socket.on('disconnect', () => {
    console.log(`User ${socket.id} disconnected from /chat`);
  });
});
// Admin namespace (restricted)
const adminNamespace = io.of('/admin');
adminNamespace.use((socket, next) => {
  // Authentication middleware
  const token = socket.handshake.auth.token;
  if (isValidAdminToken(token)) {
    next();
  } else {
    next(new Error('Authentication failed'));
  }
});
adminNamespace.on('connection', (socket) => {
  console.log('Admin connected');
  
  socket.on('admin-action', (data) => {
    // Admin-specific actions
    adminNamespace.emit('admin-update', data);
  });
});
// Notifications namespace
const notifNamespace = io.of('/notifications');
notifNamespace.on('connection', (socket) => {
  socket.on('subscribe', (userId) => {
    socket.join(`user-${userId}`);
  });
});
server.listen(3000);Connecting to Namespaces – Client Side
// Connect to different namespaces
const defaultSocket = io(); // or io('/')
const chatSocket = io('/chat');
const adminSocket = io('/admin', {
  auth: {
    token: 'admin-token-here'
  }
});
const notifSocket = io('/notifications');
// Each namespace has independent event handlers
chatSocket.on('receive-message', (data) => {
  console.log('Chat message:', data);
});
notifSocket.on('notification', (data) => {
  console.log('New notification:', data);
});
adminSocket.on('connect_error', (err) => {
  console.log('Admin auth failed:', err.message);
});Rooms: Grouping Within Namespaces
What Are Rooms?
Rooms are arbitrary channels within a namespace that sockets can join and leave. They’re server-side only constructs – clients don’t know which room they’re in.
Key characteristics:
- Lightweight and dynamic (created on-demand)
- Sockets can be in multiple rooms simultaneously
- Perfect for targeted broadcasting
- Automatically cleaned up when empty
When to Use Rooms
- Chat channels: Different topics or conversation groups
- Game lobbies: Players in the same match
- Collaborative docs: Users editing the same document
- Private messaging: One-to-one conversations
- User-specific updates: Broadcast to all sessions of a user
Complete Multi-Room Chat Implementation
Let’s build a feature-rich multi-room chat system:
// server.js - Multi-room Chat Server
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.use(express.static(path.join(__dirname, 'public')));
// Store users and rooms
const users = new Map(); // socket.id => {username, currentRoom}
const rooms = new Map(); // roomName => Set of socket.ids
io.on('connection', (socket) => {
  console.log(`User connected: ${socket.id}`);
  
  // User joins with username
  socket.on('join-server', (username) => {
    users.set(socket.id, {
      username: username,
      currentRoom: null
    });
    
    // Send available rooms list
    socket.emit('rooms-list', Array.from(rooms.keys()));
  });
  
  // Join a specific room
  socket.on('join-room', (roomName) => {
    const user = users.get(socket.id);
    if (!user) return;
    
    // Leave current room if in one
    if (user.currentRoom) {
      socket.leave(user.currentRoom);
      io.to(user.currentRoom).emit('user-left', {
        username: user.username,
        room: user.currentRoom
      });
      
      // Remove from room tracking
      const roomUsers = rooms.get(user.currentRoom);
      if (roomUsers) {
        roomUsers.delete(socket.id);
        if (roomUsers.size === 0) {
          rooms.delete(user.currentRoom);
        }
      }
    }
    
    // Join new room
    socket.join(roomName);
    user.currentRoom = roomName;
    
    // Track room membership
    if (!rooms.has(roomName)) {
      rooms.set(roomName, new Set());
    }
    rooms.get(roomName).add(socket.id);
    
    // Notify room members
    socket.to(roomName).emit('user-joined', {
      username: user.username,
      room: roomName
    });
    
    // Send room info to user
    const roomUsers = Array.from(rooms.get(roomName))
      .map(id => users.get(id)?.username)
      .filter(Boolean);
      
    socket.emit('room-joined', {
      room: roomName,
      users: roomUsers
    });
    
    console.log(`${user.username} joined ${roomName}`);
  });
  
  // Send message to current room
  socket.on('room-message', (message) => {
    const user = users.get(socket.id);
    if (!user || !user.currentRoom) return;
    
    io.to(user.currentRoom).emit('room-message', {
      username: user.username,
      message: message,
      room: user.currentRoom,
      timestamp: new Date().toISOString()
    });
  });
  
  // Private message to specific user
  socket.on('private-message', ({ targetUsername, message }) => {
    const sender = users.get(socket.id);
    if (!sender) return;
    
    // Find target socket
    for (const [socketId, userData] of users) {
      if (userData.username === targetUsername) {
        io.to(socketId).emit('private-message', {
          from: sender.username,
          message: message,
          timestamp: new Date().toISOString()
        });
        
        // Confirm to sender
        socket.emit('private-message-sent', {
          to: targetUsername,
          message: message
        });
        break;
      }
    }
  });
  
  // Create new room
  socket.on('create-room', (roomName) => {
    if (!rooms.has(roomName)) {
      rooms.set(roomName, new Set());
      io.emit('room-created', roomName);
      console.log(`Room created: ${roomName}`);
    }
  });
  
  // Get online users in current room
  socket.on('get-room-users', () => {
    const user = users.get(socket.id);
    if (!user || !user.currentRoom) return;
    
    const roomUsers = Array.from(rooms.get(user.currentRoom) || [])
      .map(id => users.get(id)?.username)
      .filter(Boolean);
      
    socket.emit('room-users', {
      room: user.currentRoom,
      users: roomUsers
    });
  });
  
  // Typing indicator for room
  socket.on('typing-start', () => {
    const user = users.get(socket.id);
    if (!user || !user.currentRoom) return;
    
    socket.to(user.currentRoom).emit('user-typing', {
      username: user.username,
      room: user.currentRoom
    });
  });
  
  socket.on('typing-stop', () => {
    const user = users.get(socket.id);
    if (!user || !user.currentRoom) return;
    
    socket.to(user.currentRoom).emit('user-stopped-typing', {
      username: user.username,
      room: user.currentRoom
    });
  });
  
  // Disconnect handling
  socket.on('disconnect', () => {
    const user = users.get(socket.id);
    if (!user) return;
    
    // Notify current room
    if (user.currentRoom) {
      socket.to(user.currentRoom).emit('user-left', {
        username: user.username,
        room: user.currentRoom
      });
      
      // Clean up room tracking
      const roomUsers = rooms.get(user.currentRoom);
      if (roomUsers) {
        roomUsers.delete(socket.id);
        if (roomUsers.size === 0) {
          rooms.delete(user.currentRoom);
          io.emit('room-deleted', user.currentRoom);
        }
      }
    }
    
    users.delete(socket.id);
    console.log(`${user.username} disconnected`);
  });
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});Room Communication Patterns
sequenceDiagram
    participant U1 as User 1
    participant Server
    participant Room as Room "javascript"
    participant U2 as User 2
    participant U3 as User 3
    
    U1->>Server: join-room("javascript")
    Server->>Room: socket.join()
    Server->>U2: user-joined event
    Server->>U3: user-joined event
    Server->>U1: room-joined (with user list)
    
    U1->>Server: room-message("Hello!")
    Server->>Room: io.to("javascript").emit()
    Room->>U1: room-message
    Room->>U2: room-message
    Room->>U3: room-message
    
    U1->>Server: private-message to User 2
    Server->>U2: private-message (only)
    
    U1->>Server: disconnect
    Server->>Room: Leave room
    Server->>U2: user-left event
    Server->>U3: user-left eventBroadcasting Strategies
Understanding different broadcasting methods is crucial:
// 1. Broadcast to ALL clients (including sender)
io.emit('event-name', data);
// 2. Broadcast to ALL except sender
socket.broadcast.emit('event-name', data);
// 3. Send to specific socket
io.to(socketId).emit('event-name', data);
// 4. Broadcast to room (including sender)
io.to('room-name').emit('event-name', data);
// 5. Broadcast to room (excluding sender)
socket.to('room-name').emit('event-name', data);
// 6. Broadcast to multiple rooms
io.to('room1').to('room2').emit('event-name', data);
// 7. Broadcast to room except certain sockets
socket.to('room-name').except(socketId).emit('event-name', data);
// 8. Broadcast within namespace
io.of('/chat').emit('event-name', data);Advanced Event Handling
Event Acknowledgments
Get confirmation that events were received:
// Server
socket.on('important-action', (data, callback) => {
  // Process action
  const result = processAction(data);
  
  // Acknowledge with result
  callback({
    success: true,
    result: result
  });
});
// Client
socket.emit('important-action', data, (response) => {
  if (response.success) {
    console.log('Action confirmed:', response.result);
  }
});Middleware for Namespaces
// Authentication middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (isValid(token)) {
    socket.userId = getUserIdFromToken(token);
    next();
  } else {
    next(new Error('Authentication error'));
  }
});
// Logging middleware
io.use((socket, next) => {
  console.log(`Connection attempt from ${socket.handshake.address}`);
  next();
});
// Rate limiting middleware
const rateLimiter = new Map();
io.use((socket, next) => {
  const ip = socket.handshake.address;
  const now = Date.now();
  const requests = rateLimiter.get(ip) || [];
  
  const recentRequests = requests.filter(time => now - time < 60000);
  
  if (recentRequests.length > 100) {
    next(new Error('Too many requests'));
  } else {
    recentRequests.push(now);
    rateLimiter.set(ip, recentRequests);
    next();
  }
});Error Handling
// Server-side error handling
socket.on('error', (error) => {
  console.error('Socket error:', error);
});
// Handle specific errors
socket.on('some-event', (data) => {
  try {
    // Process data
  } catch (error) {
    socket.emit('error', {
      message: 'Failed to process request',
      code: 'PROCESSING_ERROR'
    });
  }
});
// Client-side error handling
socket.on('connect_error', (error) => {
  console.error('Connection failed:', error.message);
});
socket.on('error', (error) => {
  console.error('Socket error:', error);
});Best Practices
- Use meaningful room names: Make them unpredictable for security (e.g., UUIDs for private rooms)
- Clean up on disconnect: Always remove users from tracking when they disconnect
- Validate input: Never trust client data – validate everything
- Limit room sizes: Consider maximum occupancy for performance
- Use namespaces for isolation: Keep unrelated features in separate namespaces
- Implement authentication: Use middleware to verify users before allowing connections
- Handle edge cases: Account for rapid joins/leaves and network issues
- Monitor room state: Keep track of active rooms and user counts
Testing Your Multi-Room Chat
- Start the server: node server.js
- Open multiple browser tabs
- Create different rooms
- Join users to different rooms
- Test broadcasting within rooms
- Test private messaging
- Test user leaving and rejoining
What’s Next
In Part 4: Authentication & Security, we’ll implement JWT authentication, session management, input validation, and rate limiting to make our application production-ready!
Part 3 of the 8-part Real-Time WebSocket Architecture Series.


 
                                     
                                     
                                         
                                         
                                        