Toolmingo
Guides10 min read

How to Use WebSockets: Complete Guide with Examples

Learn how WebSockets work, how to implement them in JavaScript, Python, Go, and PHP, and when to use WebSockets vs Server-Sent Events vs long polling — with real-world patterns and code examples.

HTTP is request-response: you ask, the server answers, connection closes. That works for loading pages. It breaks down for real-time apps — chat, live dashboards, multiplayer games — where the server needs to push data without waiting for a request.

WebSockets solve this with a persistent, full-duplex connection. Client and server can both send messages at any time, over a single TCP connection.

This guide covers how WebSockets work, how to implement them in JavaScript, Python, Go, and PHP, and when to reach for alternatives.


How WebSockets Work

WebSocket starts as an HTTP request and then upgrades to a persistent connection.

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

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

3. Both sides can now send frames at any time.
   Connection stays open until either side closes it.

After the handshake, the connection is no longer HTTP — it's a WebSocket connection over the same TCP socket.

Key properties:

  • Full-duplex: client and server send independently
  • Low overhead: no HTTP headers on every message (just a small frame header)
  • Persistent: no reconnect overhead
  • Supports text and binary frames

Quick Reference

Operation Browser JS Node.js (ws) Python (websockets) Go (gorilla/websocket)
Connect / listen new WebSocket(url) new WebSocketServer({port}) websockets.serve(handler, host, port) upgrader.Upgrade(w, r, nil)
Send text ws.send(str) ws.send(str) await ws.send(str) conn.WriteMessage(TextMessage, data)
Send JSON ws.send(JSON.stringify(obj)) same await ws.send(json.dumps(obj)) conn.WriteJSON(obj)
Receive ws.onmessage = e => ... ws.on('message', fn) msg = await ws.recv() conn.ReadMessage()
Close ws.close() ws.close() await ws.close() conn.Close()
URL scheme ws:// or wss:// (TLS) same same same

Browser WebSocket API

The browser exposes a simple WebSocket class.

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

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

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

// Connection closed
ws.addEventListener('close', (event) => {
  console.log(`Closed: code=${event.code}, reason=${event.reason}`);
  // Reconnect after 3 seconds
  setTimeout(connect, 3000);
});

// Error
ws.addEventListener('error', (err) => {
  console.error('WebSocket error:', err);
  // 'close' will fire right after
});

// Send helpers
function sendJson(obj) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(obj));
  }
}

// Close gracefully
function disconnect() {
  ws.close(1000, 'User logged out');
}

WebSocket ready states:

readyState Constant Meaning
0 CONNECTING Handshake in progress
1 OPEN Connected, can send/receive
2 CLOSING Close handshake started
3 CLOSED Connection closed

Check ws.readyState === WebSocket.OPEN before calling .send().


Node.js WebSocket Server (ws library)

// npm install ws
import { WebSocketServer } from 'ws';

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

// Track connected clients
const clients = new Set();

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

  // Receive message
  ws.on('message', (rawData) => {
    const msg = JSON.parse(rawData.toString());
    console.log('Received:', msg);

    // Broadcast to all other clients
    for (const client of clients) {
      if (client !== ws && client.readyState === 1 /* OPEN */) {
        client.send(JSON.stringify({ ...msg, from: ip }));
      }
    }
  });

  // Client disconnected
  ws.on('close', (code, reason) => {
    clients.delete(ws);
    console.log(`Client disconnected. code=${code}. Total: ${clients.size}`);
  });

  ws.on('error', (err) => {
    console.error('Client error:', err.message);
  });

  // Send welcome message
  ws.send(JSON.stringify({ type: 'welcome', clients: clients.size }));
});

console.log('WebSocket server running on ws://localhost:8080');

With Express (HTTP + WS on same port):

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

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

app.get('/', (req, res) => res.send('Hello HTTP'));

wss.on('connection', (ws) => {
  ws.send(JSON.stringify({ type: 'connected' }));
});

server.listen(3000, () => console.log('Server on http://localhost:3000'));

Python WebSocket Server (websockets library)

# pip install websockets
import asyncio
import json
import websockets

# Track clients
connected: set[websockets.WebSocketServerProtocol] = set()

async def handler(websocket: websockets.WebSocketServerProtocol):
    connected.add(websocket)
    try:
        async for raw in websocket:
            msg = json.loads(raw)
            print(f"Received: {msg}")

            # Broadcast to all other clients
            broadcast = json.dumps({**msg, "from": websocket.remote_address[0]})
            tasks = [
                client.send(broadcast)
                for client in connected
                if client is not websocket
            ]
            if tasks:
                await asyncio.gather(*tasks)
    except websockets.exceptions.ConnectionClosedError:
        pass
    finally:
        connected.discard(websocket)

async def main():
    async with websockets.serve(handler, "0.0.0.0", 8765):
        print("WebSocket server running on ws://localhost:8765")
        await asyncio.Future()  # run forever

asyncio.run(main())

Python WebSocket client:

import asyncio
import json
import websockets

async def client():
    async with websockets.connect("ws://localhost:8765") as ws:
        await ws.send(json.dumps({"type": "hello", "text": "Hi!"}))

        async for raw in ws:
            msg = json.loads(raw)
            print("Server says:", msg)

asyncio.run(client())

Go WebSocket Server (gorilla/websocket)

// go get github.com/gorilla/websocket
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // allow all origins in dev; restrict in production
    },
}

type Hub struct {
    mu      sync.Mutex
    clients map[*websocket.Conn]bool
}

func (h *Hub) add(c *websocket.Conn) {
    h.mu.Lock(); defer h.mu.Unlock()
    h.clients[c] = true
}

func (h *Hub) remove(c *websocket.Conn) {
    h.mu.Lock(); defer h.mu.Unlock()
    delete(h.clients, c)
    c.Close()
}

func (h *Hub) broadcast(msg []byte, skip *websocket.Conn) {
    h.mu.Lock(); defer h.mu.Unlock()
    for c := range h.clients {
        if c != skip {
            c.WriteMessage(websocket.TextMessage, msg)
        }
    }
}

var hub = &Hub{clients: make(map[*websocket.Conn]bool)}

func wsHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("Upgrade error:", err)
        return
    }
    hub.add(conn)
    defer hub.remove(conn)

    for {
        _, rawMsg, err := conn.ReadMessage()
        if err != nil {
            break // client disconnected
        }
        var msg map[string]any
        if err := json.Unmarshal(rawMsg, &msg); err == nil {
            fmt.Println("Received:", msg)
            hub.broadcast(rawMsg, conn)
        }
    }
}

func main() {
    http.HandleFunc("/ws", wsHandler)
    log.Println("Listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

PHP WebSocket (Ratchet)

// composer require cboden/ratchet
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

class ChatServer implements MessageComponentInterface {
    protected \SplObjectStorage $clients;

    public function __construct() {
        $this->clients = new \SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
        echo "New connection #{$conn->resourceId}\n";
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        $data = json_decode($msg, true);
        echo "Received from #{$from->resourceId}: " . print_r($data, true);

        // Broadcast to all other clients
        foreach ($this->clients as $client) {
            if ($client !== $from) {
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);
        echo "Connection #{$conn->resourceId} closed\n";
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "Error: {$e->getMessage()}\n";
        $conn->close();
    }
}

$server = IoServer::factory(
    new HttpServer(new WsServer(new ChatServer())),
    8080
);
$server->run();

Message Protocol Pattern

Raw WebSocket sends strings or bytes — you design the protocol. A common pattern uses typed JSON messages:

// Message envelope
const Message = {
  // Client → server
  join:   (room) => ({ type: 'join', room }),
  leave:  (room) => ({ type: 'leave', room }),
  chat:   (room, text) => ({ type: 'chat', room, text }),
  ping:   () => ({ type: 'ping' }),

  // Server → client
  joined:   (room, members) => ({ type: 'joined', room, members }),
  chatted:  (room, from, text, ts) => ({ type: 'chatted', room, from, text, ts }),
  error:    (code, msg) => ({ type: 'error', code, message: msg }),
  pong:     () => ({ type: 'pong' }),
};

// Dispatcher on the client
ws.addEventListener('message', ({ data }) => {
  const msg = JSON.parse(data);
  switch (msg.type) {
    case 'joined':   onJoined(msg.room, msg.members); break;
    case 'chatted':  onChatted(msg); break;
    case 'error':    onError(msg.code, msg.message); break;
    case 'pong':     lastPong = Date.now(); break;
    default:         console.warn('Unknown message type:', msg.type);
  }
});

Auto-Reconnect Pattern

WebSocket connections drop. Always reconnect with exponential backoff:

class ReconnectingWebSocket {
  #ws = null;
  #attempt = 0;
  #maxDelay = 30_000;
  #handlers = {};

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

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

    this.#ws.addEventListener('open', () => {
      this.#attempt = 0; // reset backoff on success
      this.#handlers.open?.();
    });

    this.#ws.addEventListener('message', (e) => {
      this.#handlers.message?.(e);
    });

    this.#ws.addEventListener('close', (e) => {
      this.#handlers.close?.(e);
      if (!this.#closed) {
        const delay = Math.min(1000 * 2 ** this.#attempt, this.#maxDelay);
        this.#attempt++;
        console.log(`Reconnecting in ${delay}ms (attempt ${this.#attempt})`);
        setTimeout(() => this.#connect(), delay);
      }
    });
  }

  send(data) {
    if (this.#ws?.readyState === WebSocket.OPEN) {
      this.#ws.send(typeof data === 'string' ? data : JSON.stringify(data));
    }
  }

  on(event, handler) { this.#handlers[event] = handler; }

  close() {
    this.#closed = true;
    this.#ws?.close(1000, 'Client closed');
  }
}

// Usage
const ws = new ReconnectingWebSocket('wss://api.example.com/ws');
ws.on('open', () => ws.send({ type: 'auth', token: getToken() }));
ws.on('message', (e) => dispatch(JSON.parse(e.data)));

Heartbeat / Ping-Pong

The WebSocket protocol has built-in ping/pong frames. The ws library on Node.js handles them automatically. For application-level heartbeats:

// Node.js server — kill dead clients
const INTERVAL = 30_000;

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

setInterval(() => {
  for (const ws of wss.clients) {
    if (!ws.isAlive) { ws.terminate(); continue; }
    ws.isAlive = false;
    ws.ping();
  }
}, INTERVAL);
// Browser client — send app-level ping
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 25_000);

WebSocket vs Alternatives

Technology Direction Protocol Use Case
WebSocket Full-duplex TCP (ws/wss) Chat, games, live collaboration
Server-Sent Events (SSE) Server → client only HTTP Live feeds, dashboards, notifications
Long Polling Server → client (simulated) HTTP Fallback for environments without WS
HTTP/2 Push Server → client HTTP/2 Pushed resources (CSS, JS)
gRPC streaming Both directions HTTP/2 + protobuf Microservices, high-throughput

Use SSE when: you only need server-push (activity feeds, build logs). SSE is simpler, works over plain HTTP, and auto-reconnects natively.

Use WebSocket when: you need bidirectional communication (client sends too), real-time two-way interaction, or binary data.


Security

// 1. Always use wss:// in production (TLS)
const ws = new WebSocket('wss://api.example.com/ws'); // ✓
// not ws:// over the internet

// 2. Authenticate on connection (token in query string or first message)
// Query string — visible in server logs, use with caution:
const ws = new WebSocket(`wss://api.example.com/ws?token=${token}`);

// First message (preferred — not logged):
ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'auth', token: getJwtToken() }));
});

// 3. Server must validate origin (gorilla/websocket CheckOrigin, ws verifyClient)
const wss = new WebSocketServer({
  port: 8080,
  verifyClient: ({ req }) => {
    const origin = req.headers.origin;
    return ['https://myapp.com'].includes(origin);
  },
});

// 4. Validate and sanitize all incoming messages — never trust client data
ws.on('message', (raw) => {
  let msg;
  try { msg = JSON.parse(raw.toString()); }
  catch { ws.close(1003, 'Invalid JSON'); return; }

  if (typeof msg.type !== 'string') return; // drop unknown structure
  // ... further validation
});

// 5. Rate-limit messages per client to prevent flooding

6 Common Mistakes

Mistake What Goes Wrong Fix
Sending before open event ws.send() throws or silently fails Check ws.readyState === WebSocket.OPEN
No reconnect logic Connection drops and app breaks silently Implement exponential backoff on close
No heartbeat Idle connections silently time out (NAT/proxy) Send ping every 25–30 seconds
Using ws:// in production Traffic is unencrypted Always use wss://
Skipping authentication Any client can connect Validate token on first message or upgrade
Broadcasting without room logic All clients receive all messages Track rooms/channels; filter recipients

FAQ

Can I use WebSockets with HTTP/2?

Browser WebSockets run over HTTP/1.1 upgrade. There's a newer RFC for WebSockets over HTTP/2 (RFC 8441), but browser support is limited. For HTTP/2 push patterns, consider SSE.

How many WebSocket connections can a server handle?

Each connection is a file descriptor. Linux default is 1,024; tune with ulimit -n and kernel settings. Node.js can handle tens of thousands of idle connections with proper setup. For massive scale, use a message broker (Redis Pub/Sub, NATS) to coordinate across multiple server instances.

Do WebSockets work through proxies and firewalls?

Most modern proxies handle wss:// (TLS port 443) correctly. Plain ws:// on port 80 is sometimes blocked by corporate proxies that don't understand the upgrade. Use wss:// on port 443 for maximum compatibility.

How do I scale WebSockets horizontally?

WebSocket state (which client is connected to which server) is local. When you scale to multiple processes/servers, use a pub/sub layer (Redis Pub/Sub, Socket.IO adapter) so any server can broadcast to clients on any other server.

// Redis-backed Socket.IO for horizontal scaling
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));

What's the difference between Socket.IO and raw WebSocket?

Socket.IO is a library built on top of WebSockets (with long-polling fallback). It adds rooms, namespaces, auto-reconnect, and acknowledgements. Raw WebSocket is lighter but you implement these yourself. Use Socket.IO for quick prototypes and complex pub/sub needs; use raw WebSocket when you need minimal overhead or control.

How do I debug WebSocket traffic?

In Chrome DevTools → Network tab → filter by "WS" → click the connection → Messages tab. You see each frame sent and received. For server-side, log all incoming messages with timestamps during development.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools