TECHNICAL PRESENTATION

Introduction to
WebSockets

Full-Duplex Real-Time Communication for the Web
RFC 6455 · ws:// · wss:// · frames · Socket.IO · scaling
02

Agenda

Foundations

  • The problem with HTTP polling
  • What WebSockets are (RFC 6455)
  • HTTP vs WebSockets comparison
  • The upgrade handshake
  • Frame protocol internals

Protocol Deep-Dive

  • Browser API — events & methods
  • Binary data (Blob, ArrayBuffer)
  • Node.js ws library
  • Socket.IO abstraction layer

Building Robust Apps

  • Authentication strategies
  • Heartbeats & reconnection
  • Error handling & close codes
  • Testing approaches

Production & Scale

  • Scaling horizontally
  • Load balancing & Nginx config
  • Security hardening (wss://)
  • Alternatives: SSE, WebTransport
03

The Problem — Why HTTP Polling Falls Short

Client Server GET /updates 204 No Content GET /updates 204 No Content GET /updates 200 + data (finally!) Wasted bandwidth & latency

Short Polling Problems

  • Constant HTTP overhead (headers, TCP handshake)
  • Latency = poll interval / 2 on average
  • Wasted requests when no new data
  • Server load scales with clients × poll rate

Long Polling Limitations

  • Held connections consume server threads
  • Timeout & reconnect overhead
  • Still half-duplex — one direction at a time
  • Load balancers struggle with held connections

Real-Time Demands

  • Chat, gaming, live dashboards, collaboration
  • Sub-100ms latency expectations
  • Bidirectional data flow required
  • Thousands of concurrent connections
04

What Are WebSockets?

RFC 6455 Definition

A protocol providing full-duplex communication channels over a single TCP connection. Designed to work over HTTP ports 80 and 443, and to support HTTP proxies and intermediaries.

Key Characteristics

  • Persistent connection — no repeated handshakes
  • Full-duplex — send and receive simultaneously
  • Low overhead — 2-14 byte frame header vs ~800 byte HTTP headers
  • Text & binary — supports both data types natively
  • Event-driven — push model, no polling needed

URI Schemes

  • ws:// — unencrypted (port 80)
  • wss:// — TLS encrypted (port 443)
Client Server HTTP Upgrade 101 Switching Persistent Connection message message server push message message close frame
05

HTTP vs WebSockets

AspectHTTP/1.1WebSocket
CommunicationRequest-response (half-duplex)Full-duplex, bidirectional
ConnectionNew connection per request (or keep-alive)Single persistent TCP connection
Header Overhead~800 bytes per request/response2–14 bytes per frame
LatencyTCP + TLS handshake + request timeNear-zero after initial handshake
Server PushNot natively (requires polling/SSE)Native — server pushes any time
Data FormatText (body can be binary)Text frames + binary frames
CachingBuilt-in (ETags, Cache-Control)No caching (real-time stream)
ProxiesUniversally supportedSome proxies require configuration
StatelessnessStateless by designStateful connection

Use WebSockets When

  • Real-time chat, gaming, collaboration
  • Live data feeds (stocks, sports, IoT)
  • High-frequency bidirectional messaging

Use HTTP When

  • CRUD operations, REST APIs
  • Cacheable content delivery
  • Infrequent, request-driven interactions
06

The WebSocket Handshake

The connection begins as a standard HTTP/1.1 request with an Upgrade header. The server responds with 101 Switching Protocols.

Client Request

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate
Origin: http://example.com

Server Response

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

Sec-WebSocket-Key

Random 16-byte base64 value. Server concatenates with GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, SHA-1 hashes, and base64 encodes the result.

Sec-WebSocket-Accept

Proves the server understands WebSocket. Computed as base64(SHA1(key + GUID)). Prevents cross-protocol attacks.

Subprotocols

Client proposes one or more subprotocols (e.g., graphql-ws, mqtt). Server selects one. Defines the message format on the connection.

07

Frame Protocol

FIN 1 bit RSV1-3 3 bits Opcode 4 bits MASK 1 bit Payload Length 7(+16/64) bits Masking Key 0 or 4 bytes Payload Data (XOR-masked if MASK=1) variable length

Opcodes

  • 0x0 — Continuation frame
  • 0x1 — Text frame (UTF-8)
  • 0x2 — Binary frame
  • 0x8 — Connection close
  • 0x9 — Ping
  • 0xA — Pong

Masking

  • Client → server frames MUST be masked
  • Server → client frames must NOT be masked
  • 4-byte XOR key prevents cache-poisoning attacks on proxies
  • payload[i] ^= mask[i % 4]

Payload Length Encoding

  • 0–125: 7-bit length field
  • 126: next 2 bytes are length (up to 64 KB)
  • 127: next 8 bytes are length (up to 263)
  • Minimum frame overhead: 2 bytes
08

Browser API

// Create connection
const ws = new WebSocket('wss://api.example.com/chat');

// Connection opened
ws.addEventListener('open', (event) => {
  console.log('Connected!');
  ws.send(JSON.stringify({ type: 'join', room: 'general' }));
});

// Listen for messages
ws.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
});

// Handle errors
ws.addEventListener('error', (event) => {
  console.error('WebSocket error:', event);
});

// Connection closed
ws.addEventListener('close', (event) => {
  console.log(`Closed: ${event.code} ${event.reason}`);
  if (!event.wasClean) {
    console.log('Connection lost, reconnecting...');
  }
});

// Send data
ws.send('Hello, server!');
ws.send(new Blob(['binary data']));

// Close gracefully
ws.close(1000, 'Done');

readyState Property

  • 0CONNECTING
  • 1OPEN
  • 2CLOSING
  • 3CLOSED

Key Properties

  • ws.url — resolved URL
  • ws.protocol — selected subprotocol
  • ws.bufferedAmount — bytes queued
  • ws.binaryType'blob' or 'arraybuffer'
  • ws.extensions — negotiated extensions

Event Summary

EventWhen
openConnection established
messageData received
errorError occurred
closeConnection closed
09

Binary Data

WebSockets natively support binary frames alongside text. Set ws.binaryType to control how incoming binary data is delivered.

Sending Binary Data

// ArrayBuffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, Math.PI);
ws.send(buffer);

// Typed Array
const pixels = new Uint8Array([255, 0, 128, 255]);
ws.send(pixels.buffer);

// Blob
const blob = new Blob(['Hello'], { type: 'text/plain' });
ws.send(blob);

// File from input
fileInput.onchange = (e) => {
  const file = e.target.files[0];
  ws.send(file); // sends as binary
};

Receiving Binary Data

// Set binary type
ws.binaryType = 'arraybuffer'; // or 'blob'

ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new DataView(event.data);
    const value = view.getFloat64(0);
    console.log('Received float:', value);
  }
};

// Blob approach (for large files)
ws.binaryType = 'blob';
ws.onmessage = async (event) => {
  if (event.data instanceof Blob) {
    const arrayBuf = await event.data.arrayBuffer();
    const bytes = new Uint8Array(arrayBuf);
    renderImage(bytes);
  }
};

ArrayBuffer

Fixed-length raw binary buffer. Best for structured data with known layout. Zero-copy transfer.

Blob

Immutable raw data. Better for large files — can be streamed from disk. Use URL.createObjectURL() for display.

Typed Arrays

Uint8Array, Float32Array, Int16Array, etc. Provide views into an ArrayBuffer with typed access.

10

Node.js ws Library

Basic Server

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws, req) => {
  const ip = req.socket.remoteAddress;
  console.log(`Client connected from ${ip}`);

  ws.on('message', (data, isBinary) => {
    const msg = isBinary ? data : data.toString();
    console.log('Received:', msg);

    // Echo to all clients
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(msg);
      }
    });
  });

  ws.on('close', (code, reason) => {
    console.log(`Disconnected: ${code}`);
  });

  ws.send('Welcome to the server!');
});

With Express / HTTP Server

import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });

// REST endpoints
app.get('/api/status', (req, res) => {
  res.json({
    clients: wss.clients.size,
    uptime: process.uptime()
  });
});

// WebSocket connections
wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
  ws.on('message', (msg) => {
    ws.send(`Echo: ${msg}`);
  });
});

server.listen(3000, () => {
  console.log('HTTP + WS on :3000');
});

Install

npm install ws — fastest WS lib for Node.js. Zero dependencies. ~300 KB.

Performance

Handles 50K+ concurrent connections. Use perMessageDeflate: false to reduce CPU usage on high-throughput servers.

Alternatives

uWebSockets.js for extreme perf (C++ core). Bun.serve() has built-in WS. Deno has native Deno.serve().

11

Socket.IO

Server

import { Server } from 'socket.io';

const io = new Server(3000, {
  cors: { origin: 'http://localhost:5173' }
});

// Namespaces
const chat = io.of('/chat');

chat.on('connection', (socket) => {
  // Join a room
  socket.join('room-42');

  // Listen for events
  socket.on('chat:message', (msg, callback) => {
    // Broadcast to room
    socket.to('room-42').emit('chat:message', {
      user: socket.data.username,
      text: msg,
      ts: Date.now()
    });
    callback({ status: 'ok' }); // acknowledgement
  });

  socket.on('disconnect', (reason) => {
    console.log(`Left: ${reason}`);
  });
});

Client

import { io } from 'socket.io-client';

const socket = io('http://localhost:3000/chat', {
  auth: { token: 'jwt-token-here' },
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionAttempts: 5
});

socket.on('connect', () => {
  console.log('Connected:', socket.id);
});

// Send with acknowledgement
socket.emit('chat:message', 'Hello!', (res) => {
  console.log('Server ack:', res.status);
});

// Receive events
socket.on('chat:message', (data) => {
  renderMessage(data);
});

socket.on('connect_error', (err) => {
  console.log('Auth error:', err.message);
});

Key Features

  • Rooms & Namespaces for logical grouping
  • Auto-reconnect with exponential backoff
  • Acknowledgements (request-response over WS)

Trade-offs

  • Custom protocol on top of WS — not interoperable
  • Falls back to long-polling (can be a pro or con)
  • Adds ~45 KB (minified+gzipped) to bundle
12

Authentication

Token in Query String

// Client
const ws = new WebSocket(
  `wss://api.example.com/ws?token=${jwt}`
);

// Server (Node.js ws)
wss.on('connection', (ws, req) => {
  const url = new URL(req.url, 'http://localhost');
  const token = url.searchParams.get('token');
  try {
    const user = jwt.verify(token, SECRET);
    ws.userId = user.id;
  } catch (e) {
    ws.close(4001, 'Invalid token');
  }
});

First-Message Auth

ws.on('connection', (ws) => {
  ws.isAuthenticated = false;
  ws.on('message', (data) => {
    if (!ws.isAuthenticated) {
      const { token } = JSON.parse(data);
      if (verifyToken(token)) {
        ws.isAuthenticated = true;
        ws.send(JSON.stringify({ type: 'auth_ok' }));
      } else {
        ws.close(4001, 'Unauthorized');
      }
      return;
    }
    handleMessage(ws, data);
  });
});

Cookie-Based Auth

// Cookies are sent during handshake
// Server reads them from upgrade request
import cookie from 'cookie';

wss.on('connection', (ws, req) => {
  const cookies = cookie.parse(
    req.headers.cookie || ''
  );
  const sessionId = cookies['session_id'];
  const session = await sessionStore
    .get(sessionId);

  if (!session) {
    ws.close(4001, 'No valid session');
    return;
  }
  ws.user = session.user;
});

Security Considerations

  • Query string tokens appear in server logs
  • Browser API does not support custom headers
  • Cookies require CSRF protection
  • Always use wss:// to encrypt tokens in transit
  • Set short expiry on WS-specific tokens
  • Validate origin header on the server
13

Heartbeats & Reconnection

Server-Side Ping/Pong (ws)

// Server heartbeat interval
const HEARTBEAT_MS = 30_000;

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
});

const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) {
      console.log('Terminating dead connection');
      return ws.terminate();
    }
    ws.isAlive = false;
    ws.ping(); // client auto-responds pong
  });
}, HEARTBEAT_MS);

wss.on('close', () => clearInterval(interval));

Client Reconnection with Backoff

class ReconnectingWebSocket {
  #url; #ws; #retries = 0;
  #maxRetries = 10;
  #baseDelay = 1000;

  constructor(url) {
    this.#url = url;
    this.#connect();
  }

  #connect() {
    this.#ws = new WebSocket(this.#url);

    this.#ws.onopen = () => {
      this.#retries = 0; // reset on success
    };

    this.#ws.onclose = (e) => {
      if (e.code !== 1000) this.#reconnect();
    };
  }

  #reconnect() {
    if (this.#retries >= this.#maxRetries) return;
    const delay = this.#baseDelay
      * Math.pow(2, this.#retries)
      + Math.random() * 1000; // jitter
    this.#retries++;
    setTimeout(() => this.#connect(), delay);
  }

  send(data) { this.#ws.send(data); }
}

Why Heartbeat?

NATs, proxies, and firewalls silently drop idle TCP connections (often after 60–120s). Ping/pong keeps the connection alive.

Exponential Backoff

Delays: 1s, 2s, 4s, 8s, 16s... plus random jitter. Prevents thundering herd when server restarts.

Application-Level Ping

Browser WS API cannot send protocol pings. Use application-level {"type":"ping"} messages as a fallback.

14

Error Handling

WebSocket Close Codes

CodeNameMeaning
1000NormalGraceful close
1001Going AwayServer shutdown / page nav
1002Protocol ErrorMalformed frame
1003UnsupportedUnexpected data type
1006AbnormalNo close frame (network drop)
1008Policy ViolationMessage violates policy
1009Too LargeMessage exceeds size limit
1011Server ErrorUnexpected server condition
4000–4999App-definedCustom codes for your protocol

Graceful Error Handling

ws.onerror = (err) => {
  // error event has no useful info
  // in browsers; close follows
  console.error('WS error');
};

ws.onclose = (event) => {
  switch (event.code) {
    case 1000:
      break; // normal
    case 1006:
      scheduleReconnect(); // lost
      break;
    case 4001:
      refreshToken(); // auth failed
      break;
    default:
      logAndReconnect(event);
  }
};

Common Pitfalls

  • Browser onerror event gives no details
  • Code 1006 = connection lost, not a clean close
  • Always check readyState before send()
  • Handle bufferedAmount to prevent memory leaks
15

Scaling WebSockets

Clients Load Balancer WS Node 1 WS Node 2 WS Node 3 Redis Pub/Sub All nodes publish/subscribe to Redis — messages reach every connected client

Sticky Sessions

  • WS connections are stateful — must stay on same node
  • Use IP-hash or cookie-based routing
  • Required for Socket.IO if using long-polling fallback
  • Limits even distribution under churn

Redis Pub/Sub

import Redis from 'ioredis';
const pub = new Redis();
const sub = new Redis();

sub.subscribe('chat');
sub.on('message', (ch, msg) => {
  // broadcast to local clients
  localClients.forEach(ws =>
    ws.send(msg));
});

// When a client sends a msg:
pub.publish('chat', data);

Capacity Planning

  • Each connection ~10–40 KB memory
  • Linux: set ulimit -n > 100K
  • Tune net.core.somaxconn
  • Single Node.js process: ~50K–100K connections
  • Use cluster or PM2 for multi-core
16

Load Balancing

Nginx Configuration

upstream websocket_backend {
    # Use ip_hash for sticky sessions
    ip_hash;
    server 10.0.1.1:8080;
    server 10.0.1.2:8080;
    server 10.0.1.3:8080;
}

server {
    listen 443 ssl;
    server_name ws.example.com;

    ssl_certificate     /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;

    location /ws {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;

        # Critical: forward Upgrade headers
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;

        # Timeouts for idle connections
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;

        # Forward real IP
        proxy_set_header X-Real-IP
          $remote_addr;
        proxy_set_header X-Forwarded-For
          $proxy_add_x_forwarded_for;
    }
}

Common Proxy Pitfalls

  • Default proxy_read_timeout is 60s — kills idle WS connections
  • Missing proxy_http_version 1.1 blocks upgrades
  • Missing Connection "upgrade" header = 400 errors
  • HTTP/2 backend doesn't support WS upgrade natively

HAProxy Config

frontend ws_front
    bind *:443 ssl crt /etc/ssl/cert.pem
    acl is_ws hdr(Upgrade) -i websocket
    use_backend ws_back if is_ws

backend ws_back
    balance source  # sticky
    timeout tunnel 86400s
    server ws1 10.0.1.1:8080 check
    server ws2 10.0.1.2:8080 check

Cloud Load Balancers

  • AWS ALB: native WS support, sticky via cookies
  • GCP: use TCP load balancer or backend with WS
  • Cloudflare: WS pass-through, 100s idle timeout
17

Security

Threat Model

  • Cross-Site WebSocket Hijacking — malicious page opens WS to your server using the user's cookies
  • Denial of Service — opening thousands of connections, sending huge frames
  • Data Injection — malformed messages, XSS via echoed content
  • Man-in-the-Middle — eavesdropping on ws:// traffic

Origin Checking

wss.on('headers', (headers, req) => {
  const origin = req.headers.origin;
  const allowed = [
    'https://example.com',
    'https://app.example.com'
  ];
  if (!allowed.includes(origin)) {
    req.destroy(); // reject connection
  }
});

Rate Limiting

const rateMap = new Map();

ws.on('message', (data) => {
  const now = Date.now();
  const history = rateMap.get(ws) || [];

  // Remove msgs older than 1 minute
  const recent = history.filter(
    t => now - t < 60_000
  );
  recent.push(now);
  rateMap.set(ws, recent);

  if (recent.length > 100) { // 100/min
    ws.close(4008, 'Rate limit exceeded');
    return;
  }
  handleMessage(ws, data);
});

Security Checklist

  • Always use wss:// in production
  • Validate Origin header on server
  • Authenticate during or immediately after handshake
  • Set maxPayload on server (ws: default 100 MB!)
  • Rate limit messages per connection
  • Sanitize all incoming data before echoing
  • Implement connection limits per IP
  • Use CSP connect-src directive
18

Testing WebSockets

Integration Test (Jest + ws)

import { WebSocketServer } from 'ws';
import WebSocket from 'ws';

describe('Chat Server', () => {
  let wss, serverUrl;

  beforeAll((done) => {
    wss = new WebSocketServer({ port: 0 });
    setupHandlers(wss);
    const port = wss.address().port;
    serverUrl = `ws://localhost:${port}`;
    done();
  });

  afterAll(() => wss.close());

  test('echoes messages', (done) => {
    const ws = new WebSocket(serverUrl);
    ws.on('open', () => {
      ws.send('hello');
    });
    ws.on('message', (data) => {
      expect(data.toString())
        .toBe('Echo: hello');
      ws.close();
      done();
    });
  });

  test('rejects bad auth', (done) => {
    const ws = new WebSocket(
      `${serverUrl}?token=invalid`
    );
    ws.on('close', (code) => {
      expect(code).toBe(4001);
      done();
    });
  });
});

Load Testing with k6

// k6 script: ws_load.js
import ws from 'k6/ws';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 1000 },
    { duration: '1m',  target: 1000 },
    { duration: '10s', target: 0 },
  ],
};

export default function () {
  const url = 'wss://api.example.com/ws';
  const res = ws.connect(url, {}, (socket) => {
    socket.on('open', () => {
      socket.send(JSON.stringify({
        type: 'ping'
      }));
    });
    socket.on('message', (msg) => {
      check(msg, {
        'is pong': (m) =>
          JSON.parse(m).type === 'pong',
      });
    });
    socket.setTimeout(() => {
      socket.close();
    }, 5000);
  });
  check(res, { 'status 101': (r) =>
    r && r.status === 101 });
}

Testing Tools

  • wscat — CLI WebSocket client for manual testing
  • Postman — WS tab for interactive testing
  • k6 — load testing with WS protocol support
  • Artillery — WS engine for load scenarios
  • Wireshark — packet-level WS frame inspection
19

Alternatives Compared

TechnologyDirectionTransportBinaryReconnectBest For
WebSocketFull-duplexTCPYesManualChat, gaming, collaboration
SSEServer → ClientHTTP/1.1No (text)Built-inLive feeds, notifications
Long PollingSimulated pushHTTPYesBuilt-inLegacy fallback
HTTP/2 PushServer → ClientHTTP/2YesN/AResource preloading (deprecated)
WebTransportFull-duplexQUIC/HTTP3YesBuilt-inLow-latency, unreliable streams

Server-Sent Events (SSE)

// Server (Express)
app.get('/events', (req, res) => {
  res.setHeader('Content-Type',
    'text/event-stream');
  res.setHeader('Cache-Control',
    'no-cache');
  const id = setInterval(() => {
    res.write(
      `data: ${JSON.stringify({
        time: Date.now()
      })}\n\n`
    );
  }, 1000);
  req.on('close', () =>
    clearInterval(id));
});

WebTransport (Emerging)

const transport =
  new WebTransport(
    'https://example.com/wt'
  );
await transport.ready;

// Bidirectional stream
const stream = await transport
  .createBidirectionalStream();
const writer = stream.writable
  .getWriter();
await writer.write(
  new Uint8Array([1, 2, 3])
);

Decision Guide

  • Need bidirectional? → WebSocket
  • Server-only push + auto-reconnect? → SSE
  • Need unreliable/unordered streams? → WebTransport
  • Simple, broad compatibility? → SSE or WebSocket
  • Legacy browser support? → Long Polling
20

Summary & Next Steps

Key Takeaways

  • WebSockets provide persistent, full-duplex communication over a single TCP connection
  • The protocol starts with an HTTP Upgrade handshake then switches to a lightweight frame-based protocol
  • 2–14 byte frame overhead vs ~800 byte HTTP headers
  • Native browser API with 4 events: open, message, close, error
  • Libraries like Socket.IO add rooms, namespaces, and auto-reconnect
  • Scale with Redis Pub/Sub + sticky sessions + proper load balancer config

Production Checklist

  • Always use wss:// (TLS)
  • Implement heartbeats (ping/pong)
  • Add exponential backoff reconnection
  • Validate origin & authenticate early
  • Set maxPayload & rate limits
  • Monitor connection counts & memory
  • Configure proxy timeouts (> 24h)

Next Steps

  • Build a real-time chat app with ws or Socket.IO
  • Implement a collaborative editor (OT/CRDT + WS)
  • Experiment with binary protocols (Protocol Buffers over WS)
  • Try WebTransport for next-gen real-time apps
  • Load test with k6 to find your server's connection limits
  • Explore GraphQL subscriptions over WebSocket

Resources

  • RFC 6455 — The WebSocket Protocol
  • MDN — WebSocket API reference
  • ws — github.com/websockets/ws
  • Socket.IO — socket.io/docs
  • WebTransport — W3C specification
  • k6 — k6.io/docs/using-k6/protocols/websockets
RFC 6455 ws:// Socket.IO wss://