Skip to content

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