WebSockets¶
HTTP is a request-response protocol — you ask , the server answers , the connection closes. Real-time apps (chat , notifications , live updates) don't fit that model because the server needs to push data to clients without waiting for a request WebSockets open a persistent bidirectional channel between client and server. Once the handshake completes , either side can send messages at any time. No polling , no long-polling hacks , no "refresh to see new messages"
The ws Library — Bare Metal WebSockets¶
npm install ws
// server.js — raw WebSocket server
const WebSocket = require('ws')
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('WebSocket server running')
})
const wss = new WebSocket.Server({ server })
wss.on('connection', (ws, req) => {
console.log(`Client connected from ${req.socket.remoteAddress}`)
// Send welcome message
ws.send(JSON.stringify({ type: 'welcome', message: 'Connected to server' }))
// Handle incoming messages
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString())
console.log('Received:', message)
// Echo back
ws.send(JSON.stringify({
type: 'echo',
original: message,
timestamp: Date.now()
}))
} catch (err) {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }))
}
})
// Handle disconnection
ws.on('close', (code, reason) => {
console.log(`Client disconnected (${code}): ${reason}`)
})
// Handle errors
ws.on('error', (err) => {
console.error('WebSocket error:', err.message)
})
})
server.listen(8080, () => {
console.log('Server listening on port 8080')
})
// client.js
const WebSocket = require('ws')
const ws = new WebSocket('ws://localhost:8080')
ws.on('open', () => {
console.log('Connected to server')
// Send a message
ws.send(JSON.stringify({ action: 'ping', timestamp: Date.now() }))
})
ws.on('message', (data) => {
const message = JSON.parse(data.toString())
console.log('Received:', message)
})
ws.on('close', () => {
console.log('Disconnected from server')
})
// Send heartbeat every 30 seconds
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'heartbeat' }))
}
}, 30000)
Socket.IO — WebSockets With Training Wheels¶
Socket.IO handles reconnection , rooms , namespaces , fallback transports , and broadcasting — things you'd have to build yourself with raw ws
npm install socket.io socket.io-client
// server.js
const express = require('express')
const http = require('http')
const { Server } = require('socket.io')
const app = express()
const server = http.createServer(app)
const io = new Server(server, {
cors: {
origin: ['http://localhost:3000'],
methods: ['GET', 'POST']
},
// Connection settings
pingInterval: 25000, // Ping every 25 seconds
pingTimeout: 20000, // Disconnect if no pong for 20 seconds
maxHttpBufferSize: 1e6 // Max message size: 1MB
})
// Auth middleware
io.use((socket, next) => {
const token = socket.handshake.auth.token
if (!token) {
return next(new Error('Authentication required'))
}
try {
const user = verifyToken(token)
socket.user = user
next()
} catch (err) {
next(new Error('Invalid token'))
}
})
io.on('connection', (socket) => {
console.log(`User ${socket.user?.id} connected (socket: ${socket.id})`)
// Join a room (chat room , project channel)
socket.on('join-room', (roomId) => {
socket.join(roomId)
socket.to(roomId).emit('user-joined', {
userId: socket.user.id,
username: socket.user.name
})
})
// Leave a room
socket.on('leave-room', (roomId) => {
socket.leave(roomId)
socket.to(roomId).emit('user-left', {
userId: socket.user.id,
username: socket.user.name
})
})
// Handle chat messages
socket.on('chat-message', (data) => {
const message = {
id: generateId(),
userId: socket.user.id,
username: socket.user.name,
content: data.content,
roomId: data.roomId,
timestamp: Date.now()
}
// Broadcast to room (including sender)
io.to(data.roomId).emit('new-message', message)
// Or broadcast to everyone except sender
// socket.to(data.roomId).emit('new-message', message)
})
// Handle typing indicators
socket.on('typing', (data) => {
socket.to(data.roomId).emit('user-typing', {
userId: socket.user.id,
username: socket.user.name
})
})
// Handle disconnection
socket.on('disconnect', (reason) => {
console.log(`User ${socket.user?.id} disconnected: ${reason}`)
})
})
server.listen(4000, () => {
console.log('Socket.IO server on port 4000')
})
// client.js (browser or Node)
const { io } = require('socket.io-client')
const socket = io('http://localhost:4000', {
auth: {
token: 'jwt-token-here'
},
transports: ['websocket'], // Skip long-polling , go straight to WS
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000
})
socket.on('connect', () => {
console.log('Connected:', socket.id)
socket.emit('join-room', 'room-123')
})
socket.on('connect_error', (err) => {
console.error('Connection error:', err.message)
})
socket.on('new-message', (message) => {
console.log(`${message.username}: ${message.content}`)
})
socket.on('user-joined', (data) => {
console.log(`${data.username} joined the room`)
})
// Send a message
socket.emit('chat-message', {
content: 'Hello everyone!',
roomId: 'room-123'
})
Real-Time Patterns — What You Actually Build¶
Chat — the classic: Messages go to a room , room members get them in real time Socket.IO rooms make this trivial
Live notifications:
// Server pushes notification to specific user
io.to(userSocketId).emit('notification', {
type: 'new-follower',
message: 'Someone followed you',
data: { userId: '456' }
})
Live updates (collaborative editing):
socket.on('document-change', (data) => {
// Broadcast change to other editors in the room
socket.to(data.documentId).emit('remote-change', data.change)
})
Dashboard live data:
// Push metrics to connected dashboards every 5 seconds
setInterval(() => {
const metrics = {
cpu: os.loadavg()[0],
memory: process.memoryUsage().heapUsed,
requests: requestCounter,
errors: errorCounter
}
io.to('dashboard').emit('metrics-update', metrics)
}, 5000)
Scaling WebSockets — The Hard Part¶
WebSocket connections stick to the server that accepted them. If you have 10 server instances , a user's connection lives on one instance. Broadcasting to a room requires all instances to know about all connections
Solution — Redis adapter:
const { Server } = require('socket.io')
const { createAdapter } = require('@socket.io/redis-adapter')
const { createClient } = require('redis')
const pubClient = createClient({ url: process.env.REDIS_URL })
const subClient = pubClient.duplicate()
const io = new Server()
io.adapter(createAdapter(pubClient, subClient))
With Redis adapter , messages publish to Redis channels and all server instances receive them. Broadcasting works across instances transparently
Security — Don't Let Anyone Connect¶
Origin check:
const io = new Server({
cors: {
origin: ['https://myapp.com'], // Block all other origins
credentials: true
}
})
Rate limiting:
// Rate limit connections and messages per user
const rateLimit = new Map()
io.use((socket, next) => {
const ip = socket.handshake.address
const now = Date.now()
const windowMs = 60000
if (!rateLimit.has(ip)) {
rateLimit.set(ip, [])
}
const timestamps = rateLimit.get(ip).filter(t => now - t < windowMs)
if (timestamps.length >= 20) { // Max 20 connections per minute
return next(new Error('Rate limited'))
}
timestamps.push(now)
rateLimit.set(ip, timestamps)
next()
})
CSWSH (Cross-Site WebSocket Hijacking): WebSocket connections carry cookies by default. If a user visits a malicious site , that site can open WebSocket connections to your server with the user's session cookies
Prevention:
// 1. Check Origin header on connection
const io = new Server({
allowRequest: (req, callback) => {
const origin = req.headers.origin
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true) // Allow
} else {
callback('Origin not allowed', false) // Deny
}
}
})
// 2. Use tokens instead of cookies for auth
// Pass token in connection handshake
const socket = io('http://localhost:4000', {
auth: { token: 'jwt-token' }
})
// 3. Validate token on connection
io.use((socket, next) => {
const token = socket.handshake.auth.token
if (!validateJWT(token)) {
return next(new Error('Invalid token'))
}
next()
})
next → upcoming_03_microservices.md