nodejs3 Min Read

WebSockets with Socket.io in Node.js

Gorav Singal

April 02, 2026

TL;DR

Socket.io adds rooms, namespaces, auto-reconnection, and fallback transports on top of WebSockets. Scale horizontally with the Redis adapter. Use rooms for group messaging and namespaces for feature separation.

WebSockets with Socket.io in Node.js

WebSocket vs HTTP

Traditional HTTP follows a request/response model — the client always initiates. WebSocket provides a persistent, bidirectional connection where the server can push data to clients at any time.

WebSocket vs HTTP

Socket.io Server Setup

const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: process.env.FRONTEND_URL,
    methods: ['GET', 'POST'],
  },
  pingInterval: 25000,  // Heartbeat every 25s
  pingTimeout: 20000,   // Disconnect if no pong in 20s
});

httpServer.listen(3000, () => {
  console.log('Server running on port 3000');
});

Events and Communication

io.on('connection', (socket) => {
  console.log(`Client connected: ${socket.id}`);

  // Listen for events from this client
  socket.on('chat:message', (data) => {
    console.log(`Message from ${socket.id}:`, data);

    // Emit to the sender only
    socket.emit('chat:message:received', { id: data.id, status: 'delivered' });

    // Broadcast to everyone EXCEPT the sender
    socket.broadcast.emit('chat:message', {
      ...data,
      sender: socket.id,
      timestamp: Date.now(),
    });
  });

  // Emit to ALL connected clients (including sender)
  socket.on('announcement', (data) => {
    io.emit('announcement', data);
  });

  socket.on('disconnect', (reason) => {
    console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
  });
});

Rooms and Namespaces

Rooms and namespaces let you organize connections for targeted messaging.

Socket.io Rooms and Namespaces

Rooms

io.on('connection', (socket) => {
  // Join a room
  socket.on('room:join', async (roomId) => {
    socket.join(roomId);
    console.log(`${socket.id} joined room: ${roomId}`);

    // Notify others in the room
    socket.to(roomId).emit('room:user-joined', {
      userId: socket.data.userId,
      roomId,
    });

    // Send room history to the joining user
    const history = await getChatHistory(roomId);
    socket.emit('room:history', history);
  });

  // Send message to a room
  socket.on('room:message', (data) => {
    io.to(data.roomId).emit('room:message', {
      ...data,
      sender: socket.data.userId,
      timestamp: Date.now(),
    });
  });

  // Leave a room
  socket.on('room:leave', (roomId) => {
    socket.leave(roomId);
    socket.to(roomId).emit('room:user-left', {
      userId: socket.data.userId,
    });
  });
});

Namespaces

// Chat namespace
const chatNsp = io.of('/chat');
chatNsp.on('connection', (socket) => {
  // Only chat events here
  socket.on('message', handleChatMessage);
});

// Notifications namespace
const notifyNsp = io.of('/notifications');
notifyNsp.on('connection', (socket) => {
  // Only notification events here
  socket.join(`user:${socket.data.userId}`);
});

// Send notification to a specific user
function notifyUser(userId, notification) {
  notifyNsp.to(`user:${userId}`).emit('notification', notification);
}

Authentication Middleware

const jwt = require('jsonwebtoken');

// Authentication middleware — runs before 'connection' event
io.use(async (socket, next) => {
  const token = socket.handshake.auth.token
    || socket.handshake.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return next(new Error('Authentication required'));
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    const user = await db.users.findById(payload.sub);

    if (!user) {
      return next(new Error('User not found'));
    }

    socket.data.userId = user.id;
    socket.data.userName = user.name;
    socket.data.role = user.role;
    next();
  } catch (err) {
    next(new Error('Invalid token'));
  }
});

Building a Real-Time Chat

io.on('connection', (socket) => {
  const { userId, userName } = socket.data;

  // Track online users
  onlineUsers.set(userId, socket.id);
  io.emit('users:online', Array.from(onlineUsers.keys()));

  // Typing indicators
  socket.on('typing:start', (roomId) => {
    socket.to(roomId).emit('typing:start', { userId, userName });
  });

  socket.on('typing:stop', (roomId) => {
    socket.to(roomId).emit('typing:stop', { userId });
  });

  // Messages with persistence
  socket.on('message:send', async (data) => {
    const message = await db.messages.create({
      roomId: data.roomId,
      senderId: userId,
      content: data.content,
      type: data.type || 'text',
    });

    io.to(data.roomId).emit('message:new', {
      id: message.id,
      sender: { id: userId, name: userName },
      content: message.content,
      type: message.type,
      createdAt: message.createdAt,
    });
  });

  // Read receipts
  socket.on('message:read', async ({ messageId, roomId }) => {
    await db.readReceipts.upsert({ messageId, userId });
    socket.to(roomId).emit('message:read', { messageId, userId });
  });

  socket.on('disconnect', () => {
    onlineUsers.delete(userId);
    io.emit('users:online', Array.from(onlineUsers.keys()));
  });
});

Scaling with Redis Adapter

When running multiple server instances, use the Redis adapter so events are broadcast across all nodes.

const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

io.adapter(createAdapter(pubClient, subClient));

// Now io.emit() reaches clients connected to ANY server instance

Client-Side Connection

import { io } from 'socket.io-client';

const socket = io('http://localhost:3000', {
  auth: { token: accessToken },
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionAttempts: 10,
  transports: ['websocket', 'polling'], // Prefer WebSocket
});

socket.on('connect', () => {
  console.log('Connected:', socket.id);
});

socket.on('connect_error', (err) => {
  if (err.message === 'Authentication required') {
    // Redirect to login
  }
});

socket.on('disconnect', (reason) => {
  if (reason === 'io server disconnect') {
    // Server forced disconnect — reconnect manually
    socket.connect();
  }
  // Otherwise, socket.io auto-reconnects
});

// Send messages
socket.emit('message:send', {
  roomId: 'general',
  content: 'Hello everyone!',
});

// Listen for messages
socket.on('message:new', (message) => {
  addToChat(message);
});

Key Takeaways

  1. Use rooms for dynamic groups (chat rooms, game lobbies) and namespaces for feature isolation
  2. Always add authentication middleware before the connection event
  3. Scale horizontally with the Redis adapter — events broadcast across all server instances
  4. Handle reconnection gracefully — replay missed events from a database
  5. Use socket.data to store per-connection user context
  6. Set appropriate heartbeat intervals to detect stale connections
Share

Related Posts

Testing Node.js — Unit, Integration, and E2E

Testing Node.js — Unit, Integration, and E2E

Testing Strategy A solid testing strategy follows the testing pyramid — many…

Redis — Caching, Sessions, Pub/Sub in Node.js

Redis — Caching, Sessions, Pub/Sub in Node.js

Why Redis for Node.js Redis is an in-memory data store that serves as a cache…

Database Integration — PostgreSQL with Node.js

Database Integration — PostgreSQL with Node.js

Choosing Your PostgreSQL Client Node.js has three main approaches to working…

Performance Optimization and Profiling in Node.js

Performance Optimization and Profiling in Node.js

Profiling First, Optimize Second Never optimize blindly. Always profile to find…

Node.js Architecture — Event Loop Deep Dive

Node.js Architecture — Event Loop Deep Dive

Why the Event Loop Matters Node.js runs JavaScript on a single thread, yet…

MongoDB with Mongoose — Patterns and Pitfalls

MongoDB with Mongoose — Patterns and Pitfalls

Schema Design Philosophy MongoDB schema design is fundamentally different from…

Latest Posts

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI video generation went from “cool demo” to “usable in production” in 2024-202…

AI Models in 2025 — Cost, Capabilities, and Which One to Use

AI Models in 2025 — Cost, Capabilities, and Which One to Use

Choosing the right AI model is one of the most impactful decisions you’ll make…

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

Generating one image with AI costs between $0.002 and $0.12. That might sound…

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

Two years ago, AI coding meant one thing: GitHub Copilot autocompleting your…

AI Agents Demystified — It's Just Automation With a Better Brain

AI Agents Demystified — It's Just Automation With a Better Brain

Let’s cut through the noise. If you read Twitter or LinkedIn, you’d think “AI…

Supply Chain Security — Protecting Your Software Pipeline

Supply Chain Security — Protecting Your Software Pipeline

In 2024, a single malicious contributor nearly compromised every Linux system on…