Skip to main content

How to Add Real-Time Features with Socket.io vs ws

·PkgPulse Team

TL;DR

Socket.io for browser apps with fallbacks and rooms; ws for lightweight server-to-server. Socket.io (~8M weekly downloads) adds auto-reconnect, namespaces, rooms, and broadcasting on top of WebSocket with a Socket.io-compatible client. ws (~80M downloads, used by many tools) is the raw WebSocket implementation — 10KB vs Socket.io's 200KB browser client. Use ws when you control both ends; Socket.io when you need browser support with fallbacks.

Key Takeaways

  • Socket.io: ~8M downloads — rooms, auto-reconnect, binary, browser-friendly
  • ws: ~80M downloads — raw WebSocket, minimal, server-to-server standard
  • Socket.io client: ~200KB — significant bundle cost for the browser
  • ws client: ~5KB — or use native browser WebSocket API (no package)
  • For browser apps: Socket.io saves time on reconnect, rooms, event system

// Server setup — src/server.ts
import { createServer } from 'http';
import { Server } from 'socket.io';
import express from 'express';

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

const io = new Server(httpServer, {
  cors: {
    origin: process.env.CLIENT_URL || 'http://localhost:3000',
    methods: ['GET', 'POST'],
  },
  // Transport fallback: try WebSocket first, then long-polling
  transports: ['websocket', 'polling'],
});

// Middleware — runs before connection
io.use(async (socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    const user = await verifyToken(token);
    socket.data.user = user;
    next();
  } catch {
    next(new Error('Authentication failed'));
  }
});

// Connection handler
io.on('connection', (socket) => {
  const user = socket.data.user;
  console.log(`${user.name} connected [${socket.id}]`);

  // Join user to their personal room
  socket.join(`user:${user.id}`);

  // ─── Chat Room Example ─────────────────────────────────────────
  socket.on('join-room', (roomId: string) => {
    socket.join(roomId);
    socket.to(roomId).emit('user-joined', { userId: user.id, name: user.name });
  });

  socket.on('leave-room', (roomId: string) => {
    socket.leave(roomId);
    socket.to(roomId).emit('user-left', { userId: user.id });
  });

  socket.on('chat-message', (data: { roomId: string; text: string }) => {
    const message = {
      id: crypto.randomUUID(),
      userId: user.id,
      name: user.name,
      text: data.text,
      timestamp: new Date().toISOString(),
    };

    // Broadcast to everyone in room (including sender)
    io.to(data.roomId).emit('chat-message', message);

    // Save to DB
    db.message.create({ data: { ...message, roomId: data.roomId } });
  });

  // ─── Typing Indicator ──────────────────────────────────────────
  socket.on('typing-start', (roomId: string) => {
    socket.to(roomId).emit('user-typing', user.id);  // Not to sender
  });

  socket.on('typing-stop', (roomId: string) => {
    socket.to(roomId).emit('user-stopped-typing', user.id);
  });

  // ─── Disconnect ───────────────────────────────────────────────
  socket.on('disconnect', (reason) => {
    console.log(`${user.name} disconnected: ${reason}`);
  });
});

// Emit to specific user from outside connection handler:
function notifyUser(userId: string, event: string, data: unknown) {
  io.to(`user:${userId}`).emit(event, data);
}

httpServer.listen(3001);
// React client — src/hooks/useSocket.ts
import { useEffect, useRef, useState } from 'react';
import { io, Socket } from 'socket.io-client';

export function useSocket(token: string) {
  const socketRef = useRef<Socket | null>(null);
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    const socket = io(process.env.NEXT_PUBLIC_WS_URL!, {
      auth: { token },
      transports: ['websocket', 'polling'],
    });

    socket.on('connect', () => setConnected(true));
    socket.on('disconnect', () => setConnected(false));

    socketRef.current = socket;
    return () => { socket.disconnect(); };
  }, [token]);

  return { socket: socketRef.current, connected };
}

// Usage in component:
function ChatRoom({ roomId, token }: { roomId: string; token: string }) {
  const { socket, connected } = useSocket(token);
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    if (!socket) return;

    socket.emit('join-room', roomId);

    socket.on('chat-message', (msg: Message) => {
      setMessages(prev => [...prev, msg]);
    });

    return () => {
      socket.emit('leave-room', roomId);
      socket.off('chat-message');
    };
  }, [socket, roomId]);

  const sendMessage = (text: string) => {
    socket?.emit('chat-message', { roomId, text });
  };

  return (
    <div>
      <div>{connected ? '🟢 Connected' : '🔴 Disconnected'}</div>
      {messages.map(m => <div key={m.id}>{m.name}: {m.text}</div>)}
      <button onClick={() => sendMessage('Hello!')}>Send</button>
    </div>
  );
}

ws (Raw WebSocket)

// ws — minimal WebSocket for Node.js (server-to-server, lightweight)
import { WebSocketServer, WebSocket } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

// Track connected clients
const clients = new Map<string, WebSocket>();

wss.on('connection', (ws, request) => {
  const clientId = crypto.randomUUID();
  clients.set(clientId, ws);

  ws.on('message', (data) => {
    const message = JSON.parse(data.toString()) as {
      type: string;
      payload: unknown;
    };

    switch (message.type) {
      case 'broadcast':
        // Broadcast to all connected clients
        clients.forEach((client) => {
          if (client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify({
              type: 'broadcast',
              from: clientId,
              payload: message.payload,
            }));
          }
        });
        break;

      case 'ping':
        ws.send(JSON.stringify({ type: 'pong' }));
        break;
    }
  });

  ws.on('close', () => {
    clients.delete(clientId);
  });

  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
    clients.delete(clientId);
  });

  // Send initial connection ack
  ws.send(JSON.stringify({ type: 'connected', id: clientId }));
});
// Native browser WebSocket (no package needed)
const ws = new WebSocket('wss://api.example.com/ws');

ws.onopen = () => {
  console.log('Connected');
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === 'price-update') {
    updateUI(data.payload);
  }
};

ws.onclose = () => {
  console.log('Disconnected — reconnecting in 3s...');
  setTimeout(() => reconnect(), 3000);
};

When to Choose

ScenarioPick
Browser chat appSocket.io
Live notifications in web appSocket.io
Need rooms/namespacesSocket.io
Fallback for browsers without WebSocketSocket.io
Server-to-server streamingws
Minimal bundle (browser)ws or native WebSocket
Internal microservice messagingws or use MQTT/AMQP instead
Next.js App Router (server components)Server-Sent Events (SSE) for read-only

Compare realtime library health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.