WebSockets
Full-duplex real-time communication — Socket.IO, rooms, and scaling.
Overview
WebSockets provide full-duplex (bidirectional), persistent communication between a browser and server over a single TCP connection. Unlike HTTP (which requires a new request for every message), a WebSocket connection stays open — the server can push data to the client any time. A WebSocket connection starts as a regular HTTP request with an 'Upgrade: websocket' header. The server responds with HTTP 101 Switching Protocols, and from that point on, both sides communicate using the WebSocket protocol (ws:// or wss:// for secure).
Socket.IO is the most popular Node.js library for real-time applications. It wraps the WebSocket protocol and adds important features: automatic reconnection, rooms (named groups of sockets), namespaces (separate communication channels), acknowledgements (confirm message received), and fallback to HTTP long-polling when WebSockets are blocked. Server-Sent Events (SSE) is a simpler alternative for one-directional server→client streaming over HTTP — easier to set up but no client→server messaging.
Real-world WebSocket use cases: live chat, real-time notifications, collaborative editing (Google Docs-style), live sports scores, multiplayer games, stock tickers, and live dashboards. For multi-server deployments, you need a Redis adapter so that when a message is broadcast, it reaches clients connected to ALL server instances, not just the one that sent it. Socket.IO's Redis adapter uses Redis Pub/Sub internally to propagate events across instances.
Code Example
const http = require('http')
const express = require('express')
const { Server } = require('socket.io')
const { createAdapter } = require('@socket.io/redis-adapter')
const Redis = require('ioredis')
const app = express()
const httpServer = http.createServer(app)
const io = new Server(httpServer, {
cors: { origin: process.env.CLIENT_URL, credentials: true }
})
// Redis adapter: sync events across multiple server instances
const pubClient = new Redis(process.env.REDIS_URL)
const subClient = pubClient.duplicate()
io.adapter(createAdapter(pubClient, subClient))
// Auth middleware: runs before connection is established
io.use((socket, next) => {
const token = socket.handshake.auth.token
const user = verifyJWT(token)
if (!user) return next(new Error('Authentication failed'))
socket.user = user
next()
})
// Connection handler
io.on('connection', (socket) => {
console.log(`✅ ${socket.user.name} connected [${socket.id}]`)
// Join a room (e.g., a chat room by ID)
socket.on('join-room', (roomId) => {
socket.join(roomId)
// Notify others in room (not the joiner)
socket.to(roomId).emit('user-joined', {
name: socket.user.name,
at: new Date().toISOString()
})
console.log(`${socket.user.name} joined room: ${roomId}`)
})
// Handle incoming message
socket.on('send-message', ({ roomId, text }) => {
const message = {
id: Date.now(),
text,
from: socket.user.name,
userId: socket.user.id,
at: new Date().toISOString()
}
// Send to ALL sockets in the room (including sender)
io.to(roomId).emit('new-message', message)
// Save to DB
saveMessage(roomId, message)
})
// Typing indicator
socket.on('typing', ({ roomId }) => {
socket.to(roomId).emit('user-typing', { name: socket.user.name })
})
socket.on('disconnect', () => {
console.log(`❌ ${socket.user.name} disconnected`)
})
})
httpServer.listen(3001, () => console.log('Socket.IO server on port 3001'))Test Your Knowledge
Ready to test what you've learned? Take the quiz with 10 questions and see your score.