WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
Networking

WebSockets Explained: Full-Duplex Communication in the Browser

HTTP is a request-response protocol: the client asks, the server answers, and the connection closes. For applications that need continuous data flow — chat, live dashboards, collaborative editing, multiplayer games — the overhead of repeated HTTP round trips is prohibitive. WebSockets solve this by upgrading a single HTTP connection into a persistent channel where either side can send data at any time.

Published June 30, 2026

Before WebSockets, developers used polling (repeated HTTP requests on a timer), long polling (holding an HTTP request open until the server has data), or plugins like Flash. Each approach worked around the fundamental limitation of HTTP rather than fixing it. WebSockets, standardized in RFC 6455 (2011), provide a first-class protocol for bidirectional, low-latency communication over a single TCP connection.

The WebSocket handshake

A WebSocket connection starts as a regular HTTP/1.1 request with a special Upgrade header. If the server supports WebSockets, it responds with 101 Switching Protocols and the connection is promoted. All subsequent communication uses the WebSocket framing protocol, not HTTP.

Client request:
    GET /chat HTTP/1.1
    Host: example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13

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

The Sec-WebSocket-Key and Sec-WebSocket-Accept handshake prevents a non-WebSocket server from accidentally accepting a WebSocket connection: the server must compute the correct hash of the key to complete the upgrade. After the handshake, the underlying TCP socket is held open indefinitely.

Client-side API

The browser WebSocket API is event-driven. You open a connection and register handlers for open, message, error, and close.

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

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

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

ws.addEventListener('close', (event) => {
    console.log(`Closed: code=${event.code} reason=${event.reason}`);
});

ws.addEventListener('error', (err) => {
    console.error('WebSocket error', err);
});

function sendMessage(text) {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: 'message', text }));
    }
}

Use wss:// (WebSocket Secure) rather than ws:// for any production application. wss:// runs WebSocket over TLS, providing the same confidentiality and integrity guarantees as HTTPS.

Server-side example

A minimal WebSocket server in Python using the websockets library broadcasts messages from any connected client to all others:

import asyncio
import websockets

connected_clients = set()

async def handler(websocket):
    connected_clients.add(websocket)
    try:
        async for message in websocket:
            recipients = connected_clients - {websocket}
            if recipients:
                await asyncio.gather(*[r.send(message) for r in recipients])
    finally:
        connected_clients.discard(websocket)

async def main():
    async with websockets.serve(handler, "0.0.0.0", 8765):
        await asyncio.get_event_loop().run_forever()

asyncio.run(main())

Because each client is a persistent connection, the server holds an open file descriptor per connected user. A server supporting 10,000 simultaneous users holds 10,000 open file descriptors. Traditional thread-per-connection servers are impractical at this scale; async I/O frameworks (Node.js, asyncio, Go goroutines) handle large connection counts efficiently.

Heartbeats and reconnection

TCP connections can silently die: a mobile client moves between networks, a NAT gateway drops idle connections, a server restarts. The WebSocket protocol includes ping/pong frames for detecting dead connections. The server sends a ping; the client must respond with a pong. A client that does not respond within a timeout is considered disconnected.

// Client-side reconnection with exponential backoff
function connectWithRetry(url, attempt = 0) {
    const ws = new WebSocket(url);
    const MAX_DELAY = 30000;
    const BASE_DELAY = 1000;

    ws.addEventListener('open', () => { attempt = 0; });

    ws.addEventListener('close', (event) => {
        if (!event.wasClean) {
            const delay = Math.min(BASE_DELAY * 2 ** attempt, MAX_DELAY);
            setTimeout(() => connectWithRetry(url, attempt + 1), delay);
        }
    });

    return ws;
}

WebSockets vs alternatives

WebSockets are the right tool when bidirectional communication is genuinely needed. Several alternatives suit simpler scenarios:

  • Server-Sent Events (SSE): a standard that lets the server push a stream of events over a regular HTTP connection. The client cannot send data through the same stream. Simpler to implement and automatically reconnects; sufficient for most dashboard and notification use cases.
  • HTTP/2 server push: the server pushes resources the client will likely need, before the client asks. Intended for speeding up page load, not for application-level messaging.
  • Long polling: the client opens an HTTP request; the server holds it open until it has data, then responds. The client immediately opens another request. Simulates push over HTTP; higher overhead than WebSockets but works through any proxy.

If the application needs the server to push data and the client does not need to send data back (activity feeds, log streams, stock tickers), SSE is simpler than WebSockets. If both sides need to send messages at any time (chat, collaborative editing, game state), WebSockets are the right choice. For most CRUD applications, neither is needed: standard HTTP with caching is cheaper and simpler.