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
WebSocketAPI (no package) - For browser apps: Socket.io saves time on reconnect, rooms, event system
Socket.io (Full-Featured)
// 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
| Scenario | Pick |
|---|---|
| Browser chat app | Socket.io |
| Live notifications in web app | Socket.io |
| Need rooms/namespaces | Socket.io |
| Fallback for browsers without WebSocket | Socket.io |
| Server-to-server streaming | ws |
| Minimal bundle (browser) | ws or native WebSocket |
| Internal microservice messaging | ws or use MQTT/AMQP instead |
| Next.js App Router (server components) | Server-Sent Events (SSE) for read-only |
Compare realtime library health on PkgPulse.
See the live comparison
View socketio vs. ws on PkgPulse →