Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Real-Time Communication with WebSockets
Traditional HTTP is request-response based — the client must initiate every interaction. It is like a walkie-talkie where only one side can press the button: the client asks, the server answers, and the connection closes. If the server has something new to tell the client, it has to wait until the client asks again.
WebSockets flip this model. They establish a persistent, bidirectional connection between client and server — more like a phone call where both sides can talk at any time. Once the connection is open, either side can send messages instantly without the overhead of establishing a new connection each time.
HTTP vs WebSockets
| Feature | HTTP | WebSockets |
|---|
| Connection | New connection per request | Persistent connection |
| Direction | Client → Server (request/response) | Bidirectional |
| Overhead | Headers sent every time | Low overhead after handshake |
| Use Case | Traditional web pages, REST APIs | Chat, gaming, live updates |
| Latency | Higher | Very low |
When to Use WebSockets
✅ Perfect for:
- Chat applications
- Live notifications
- Real-time dashboards
- Multiplayer games
- Collaborative editing (Google Docs style)
- Live sports scores
- Stock tickers
❌ Not needed for:
- Static websites
- CRUD operations
- File uploads
- SEO-focused content
Socket.io Overview
Socket.io is the most popular WebSocket library for Node.js. It is not a pure WebSocket implementation — it is a higher-level abstraction that uses WebSockets as its primary transport but gracefully falls back to HTTP long-polling when WebSockets are unavailable (behind certain corporate proxies or older load balancers).
Key capabilities:
- Automatic reconnection — if the connection drops, the client silently reconnects with exponential backoff
- Fallback to HTTP long-polling — works even where raw WebSockets are blocked
- Room and namespace support — organize connections into logical groups
- Broadcasting capabilities — send a message to all connected clients, or all except one
- Binary data support — send ArrayBuffers and Blobs, not just strings
- Acknowledgments — request-response style patterns over the persistent connection
Basic Setup
Server Side
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: "http://localhost:3000",
methods: ["GET", "POST"]
}
});
// 'connection' fires once for each new client that connects.
// Each socket object represents one specific client connection.
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
// Listen for custom 'message' events from this client.
// Event names are arbitrary strings -- you define the protocol.
socket.on('message', (data) => {
console.log('Received:', data);
// Three different ways to send data -- know which one to use:
// 1. io.emit() -- Send to ALL connected clients (including sender)
io.emit('message', data);
// 2. socket.broadcast.emit() -- Send to all EXCEPT the sender
// (useful for chat: "John is typing..." should not show to John)
socket.broadcast.emit('message', data);
// 3. socket.emit() -- Send ONLY to this specific client
// (useful for private confirmations or error responses)
socket.emit('message', data);
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
httpServer.listen(3000, () => {
console.log('Server running on port 3000');
});
Client Side
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('message', (data) => {
console.log('Received:', data);
});
// Send message
socket.emit('message', 'Hello from client!');
</script>
Building a Chat Application
Server
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);
app.use(express.static('public'));
// Store connected users
const users = new Map();
io.on('connection', (socket) => {
console.log('New connection:', socket.id);
// User joins with username
socket.on('join', (username) => {
users.set(socket.id, { username, id: socket.id });
// Notify all users
io.emit('user-joined', {
user: username,
users: Array.from(users.values())
});
// Send chat history to new user (in production, fetch from DB)
socket.emit('chat-history', []);
});
// Handle chat messages
socket.on('chat-message', (data) => {
const user = users.get(socket.id);
if (!user) return;
const message = {
id: Date.now(),
user: user.username,
text: data.text,
timestamp: new Date().toISOString()
};
// Broadcast to all clients
io.emit('chat-message', message);
});
// Typing indicator
socket.on('typing', () => {
const user = users.get(socket.id);
if (user) {
socket.broadcast.emit('user-typing', user.username);
}
});
socket.on('stop-typing', () => {
socket.broadcast.emit('user-stop-typing');
});
// User disconnects
socket.on('disconnect', () => {
const user = users.get(socket.id);
users.delete(socket.id);
if (user) {
io.emit('user-left', {
user: user.username,
users: Array.from(users.values())
});
}
});
});
httpServer.listen(3000);
Client (React Example)
import { useEffect, useState, useRef } from 'react';
import io from 'socket.io-client';
function Chat({ username }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [users, setUsers] = useState([]);
const [typing, setTyping] = useState('');
const socketRef = useRef();
useEffect(() => {
socketRef.current = io('http://localhost:3000');
socketRef.current.emit('join', username);
socketRef.current.on('chat-message', (message) => {
setMessages(prev => [...prev, message]);
});
socketRef.current.on('user-joined', ({ users }) => {
setUsers(users);
});
socketRef.current.on('user-left', ({ users }) => {
setUsers(users);
});
socketRef.current.on('user-typing', (user) => {
setTyping(`${user} is typing...`);
});
socketRef.current.on('user-stop-typing', () => {
setTyping('');
});
return () => socketRef.current.disconnect();
}, [username]);
const sendMessage = (e) => {
e.preventDefault();
if (!input.trim()) return;
socketRef.current.emit('chat-message', { text: input });
setInput('');
socketRef.current.emit('stop-typing');
};
const handleTyping = (e) => {
setInput(e.target.value);
socketRef.current.emit('typing');
// Debounce stop-typing
clearTimeout(window.typingTimeout);
window.typingTimeout = setTimeout(() => {
socketRef.current.emit('stop-typing');
}, 1000);
};
return (
<div className="chat">
<div className="users">
<h3>Online ({users.length})</h3>
{users.map(u => <div key={u.id}>{u.username}</div>)}
</div>
<div className="messages">
{messages.map(msg => (
<div key={msg.id} className="message">
<strong>{msg.user}:</strong> {msg.text}
</div>
))}
{typing && <div className="typing">{typing}</div>}
</div>
<form onSubmit={sendMessage}>
<input
value={input}
onChange={handleTyping}
placeholder="Type a message..."
/>
<button type="submit">Send</button>
</form>
</div>
);
}
Rooms and Namespaces
Rooms
Rooms are arbitrary channels that sockets can join and leave. Think of them like chat rooms in a building: each room has a name, people can walk into and out of any room, and when someone speaks in a room, only the people in that room hear it. A single socket can be in multiple rooms simultaneously.
io.on('connection', (socket) => {
// Join a room
socket.on('join-room', (roomId) => {
socket.join(roomId);
socket.to(roomId).emit('user-joined', socket.id);
});
// Leave a room
socket.on('leave-room', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user-left', socket.id);
});
// Send to room
socket.on('room-message', ({ roomId, message }) => {
io.to(roomId).emit('room-message', message);
});
});
// Server-side: send to specific room
io.to('room123').emit('notification', 'Hello room!');
// Send to multiple rooms
io.to('room1').to('room2').emit('announcement', 'Hi everyone!');
Namespaces
Namespaces provide separation of concerns at a higher level than rooms. If rooms are like rooms within a building, namespaces are like separate buildings entirely — each with its own connection, middleware, and event handlers. A client must explicitly connect to a namespace; they do not receive events from other namespaces.
// Default namespace
io.on('connection', (socket) => {
// ...
});
// Custom namespaces
const adminNamespace = io.of('/admin');
const chatNamespace = io.of('/chat');
adminNamespace.on('connection', (socket) => {
console.log('Admin connected');
// Admin-only events
});
chatNamespace.on('connection', (socket) => {
console.log('Chat user connected');
// Chat-specific events
});
// Client connects to namespace
const adminSocket = io('http://localhost:3000/admin');
const chatSocket = io('http://localhost:3000/chat');
Authentication
WebSocket connections do not support custom HTTP headers the same way REST requests do. You cannot just add an Authorization header to the WebSocket handshake from a browser. Instead, Socket.io provides the auth option on the client, which sends credentials during the handshake as part of the initial HTTP upgrade request.
The server uses middleware (similar to Express middleware, but for socket connections) to verify the token before the connection is established. If authentication fails, the connection is rejected before any events can be exchanged.
// Server-side authentication middleware -- runs once per connection attempt,
// BEFORE the 'connection' event fires. If next() is called with an error,
// the client receives a 'connect_error' event and the connection is refused.
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.user = decoded; // Attach user info to the socket for later use
next(); // Allow the connection
} catch (err) {
next(new Error('Authentication failed')); // Reject the connection
}
});
io.on('connection', (socket) => {
console.log('Authenticated user:', socket.user.id);
});
// Client
const socket = io('http://localhost:3000', {
auth: {
token: 'your-jwt-token'
}
});
socket.on('connect_error', (err) => {
if (err.message === 'Authentication failed') {
// Handle auth error
}
});
Scaling with Redis
Here is a critical limitation to understand: Socket.io connections are in-memory, tied to the specific server process that accepted the handshake. If you run two server instances behind a load balancer, a client connected to Server A cannot receive events emitted by Server B — because Server B does not know about that client.
The Redis adapter solves this by using Redis Pub/Sub as a message bus between all server instances. When any server emits an event, it publishes to Redis, and all other servers pick it up and forward it to their locally connected clients. The clients have no idea this coordination is happening.
npm install @socket.io/redis-adapter redis
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
httpServer.listen(3000);
});
Real-Time Notifications System
// Notification service
class NotificationService {
constructor(io) {
this.io = io;
this.userSockets = new Map(); // userId -> socket.id
}
registerUser(userId, socketId) {
this.userSockets.set(userId, socketId);
}
unregisterUser(userId) {
this.userSockets.delete(userId);
}
sendToUser(userId, event, data) {
const socketId = this.userSockets.get(userId);
if (socketId) {
this.io.to(socketId).emit(event, data);
}
}
sendToUsers(userIds, event, data) {
userIds.forEach(userId => this.sendToUser(userId, event, data));
}
broadcast(event, data) {
this.io.emit(event, data);
}
}
// Usage
const notifications = new NotificationService(io);
io.on('connection', (socket) => {
socket.on('register', (userId) => {
notifications.registerUser(userId, socket.id);
});
socket.on('disconnect', () => {
// Find and remove user
});
});
// Send notification from anywhere in your app
notifications.sendToUser('user123', 'notification', {
title: 'New message',
body: 'You have a new message from John'
});
Error Handling
io.on('connection', (socket) => {
socket.on('message', async (data, callback) => {
try {
// Process message
const result = await processMessage(data);
// Acknowledge success
if (callback) callback({ success: true, data: result });
} catch (error) {
console.error('Message error:', error);
// Send error to client
if (callback) {
callback({ success: false, error: error.message });
} else {
socket.emit('error', { message: error.message });
}
}
});
// Global error handler
socket.on('error', (err) => {
console.error('Socket error:', err);
});
});
// Client with acknowledgment
socket.emit('message', data, (response) => {
if (response.success) {
console.log('Message sent:', response.data);
} else {
console.error('Error:', response.error);
}
});
Summary
- WebSockets enable real-time bidirectional communication
- Socket.io provides a robust abstraction with fallbacks
- Use rooms for group messaging (chat rooms, channels)
- Use namespaces to separate different features
- Implement authentication middleware for security
- Use Redis adapter for multi-server scaling
- Handle errors with acknowledgments and try/catch
- Build typing indicators, presence, and notifications