Skip to main content
TF
10 min readArticle

What Is a WebSocket? A Practical Developer Guide

TF
ToolsFuel Team
Web development tools & tips
Code on multiple monitors representing real-time data connections

Photo by Caspar Camille Rubin on Unsplash

The Chat App That Refreshed Every 5 Seconds

My first attempt at a real-time chat feature was embarrassing in retrospect. I built it with `setInterval` — polling the server every 5 seconds for new messages. It worked. Sort of. The messages had a 0-to-5-second delay. The server was handling about 200 requests per second with 40 active users. And every time I looked at the Network tab, I had this ticking clock of `/api/messages?since=...` requests firing over and over, most returning empty arrays.

When my manager asked why the server load was high with only 40 users, I didn't have a great answer.


The correct solution was WebSockets. But I'd been avoiding them because they seemed more complex than HTTP requests, and I wasn't sure I actually understood what they were doing differently.


Here's what I learned when I finally dug into the spec.

HTTP's Fundamental Problem for Real-Time

Regular HTTP is request-response. The client asks, the server answers, the connection closes (or goes back to a pool). The server has no way to send data to the client without the client asking first.

This is fine for almost everything the web was originally built for — loading pages, submitting forms, fetching resources. The client always initiates.


But for real-time scenarios — chat messages, live price updates, collaborative editing, game state, notifications — you need the server to push data to the client the moment something happens. Not when the client asks. When it happens.


There are a few ways to fake this with HTTP:


**Short polling** — What I was doing. Client asks `"any new messages?"` every N seconds. Simple. Works everywhere. Wasteful. Most responses will be empty. Every poll is a full HTTP round trip.


**Long polling** — Client sends a request, server holds the connection open until there's something to send (or a timeout). When data arrives, server responds and the client immediately opens a new long-poll request. Less wasteful than short polling — fewer empty responses — but still has overhead per message (new connection for each response). Works everywhere, including older mobile browsers.


**Server-Sent Events (SSE)** — HTTP response that stays open indefinitely, streaming data as `text/event-stream`. Server can push data to client. Client can't push back (unidirectional). Simple to implement, auto-reconnects, uses regular HTTP/2. Good for notifications, live feeds, log streaming.


**WebSockets** — Full bidirectional persistent connection. Both sides can send messages at any time. True real-time, low overhead per message after the initial connection. The right choice when you need two-way real-time communication.


The choice between these isn't always WebSockets. I'll get back to that at the end.

The WebSocket Handshake — How It Actually Starts

Smartphone showing network connection and data exchange

Photo by Rodion Kutsaiev on Unsplash

A WebSocket connection starts as an HTTP request. This is the clever part — WebSockets reuse the existing HTTP infrastructure, which means they work through proxies and firewalls that already allow HTTP traffic.

Here's the actual handshake exchange:


```http # Step 1: Client sends an HTTP upgrade request GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13


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


After this 101 response, the HTTP protocol is gone. The same TCP connection is now speaking the WebSocket protocol — a completely different framing format designed for low-overhead bidirectional messages.


The `Sec-WebSocket-Key` and `Sec-WebSocket-Accept` values are a simple challenge-response to prevent cached HTTP responses from being misinterpreted as a WebSocket handshake. The server takes the client's key, appends a specific UUID (`258EAFA5-E914-47DA-95CA-C5AB0DC85B11` — it's in the spec), SHA-1 hashes it, and Base64-encodes the result. If you want to understand what Base64 encoding is doing in that step, I covered the algorithm in the post on
Base64 encoding and when to use it.

Once the connection upgrades, messages are sent as WebSocket frames — a compact binary format with a 2-16 byte header and a payload. No headers, no cookies, no HTTP overhead per message. Just the data.


The WebSocket protocol itself is defined in
RFC 6455 — worth reading if you're implementing anything custom, though most developers just use a library.

Using WebSockets in the Browser

The browser's WebSocket API is straightforward:

```javascript // Connect const ws = new WebSocket('wss://example.com/chat'); // Note: wss:// is WebSocket over TLS (like https). Use ws:// for unencrypted (dev only).


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


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


ws.addEventListener('close', (event) => { console.log('Disconnected', event.code, event.reason); // Implement reconnection logic here });


ws.addEventListener('error', (event) => { console.error('WebSocket error', event); // Note: error event is usually followed by close });


// Send a message ws.send(JSON.stringify({ type: 'message', text: 'Hello!' }));


// Close the connection ws.close(); ```


A few things I wish I'd known earlier:


**The message payload is a string (or ArrayBuffer for binary).** There's no built-in JSON framing — you `JSON.stringify` before sending and `JSON.parse` after receiving. Every chat app and game you've ever seen does this. There's no standard message format, which is part of why every WebSocket server implementation ends up rolling its own message protocol.


**Reconnection is not built in.** If the connection drops — and it will drop, on mobile especially — your `close` handler fires and nothing reconnects automatically. You need to implement exponential backoff reconnection logic yourself, or use a library like Socket.IO or Ably that handles this.


**The `readyState` property** tells you connection status: 0 (CONNECTING), 1 (OPEN), 2 (CLOSING), 3 (CLOSED). Always check `ws.readyState === WebSocket.OPEN` before calling `ws.send` — sending on a non-open connection throws.


**There's no built-in message acknowledgment.** If you need guaranteed delivery (message definitely arrived at the other end), you need to build an ack system in your application protocol — send a message, wait for an ack message back, retry if no ack within N seconds. This is how chat apps handle message delivery guarantees.


For server-side, Node.js's `ws` package is the standard minimal library. Socket.IO is the higher-level option that handles reconnection, rooms, namespaces, and fallback to long polling when WebSockets aren't available — useful for production apps where you need to support a wide range of client environments.

WebSockets vs HTTP Polling vs SSE — When to Use What

Here's the decision framework I actually use:

**Use HTTP short polling when:** - You need a quick prototype and real-time latency doesn't matter much - Your "real-time" update is actually fine at 30-60 second intervals (dashboards, background sync) - You need zero server complexity - Legacy infrastructure that can't handle persistent connections


**Use Server-Sent Events (SSE) when:** - Data only flows server-to-client (notifications, live feeds, log streaming) - You want simple implementation with automatic reconnection built in - You're on HTTP/2 (SSE multiplexes over existing connections efficiently) - You need browser native support without a library


```javascript // SSE client — simpler than WebSockets for one-way data const es = new EventSource('/api/live-updates'); es.addEventListener('message', (event) => { console.log(event.data); }); // Auto-reconnects on disconnect! ```


**Use WebSockets when:** - You need true bidirectional real-time (chat, collaborative editing, multiplayer games) - Client needs to send frequent small messages (typing indicators, cursor positions, game inputs) - You need sub-200ms latency and polling overhead is measurable - You're building something where connection state matters (user is typing, user went offline)


**The question I ask first:** does the client need to send data frequently and unpredictably to the server? If yes, WebSockets. If the client mostly receives data and only occasionally sends (like a notification bell), SSE is simpler and often sufficient.


For the chat app I mentioned at the start — WebSockets was the right call because users send messages (client-to-server) as often as they receive them. But a "live score ticker" on a sports site? SSE. The client never sends anything.


I've seen developers reach for WebSockets as the default for any "real-time" feature, and then spend a week debugging connection drops and reconnection edge cases that SSE would've handled automatically. Over-engineering real-time features is a real pattern. Start with the simplest option that fits your data flow.

Connection Management in Production

The conceptual side of WebSockets is clean. Production is where things get tricky.

**Scaling** — A WebSocket connection is a persistent TCP connection. A server can hold thousands of them, but they all live on one machine. To scale horizontally (multiple server instances), you need to handle the case where a message from client A (connected to server 1) needs to reach client B (connected to server 2). The standard solution is a pub/sub layer — usually Redis with pub/sub channels. Server 1 publishes to a channel, server 2 is subscribed and pushes to its connected clients.


**Load balancers** — WebSocket connections need sticky sessions or the load balancer needs to be configured to pass through the WebSocket protocol. Some load balancers don't handle the 101 upgrade correctly without configuration. Nginx needs `proxy_http_version 1.1` and `proxy_set_header Upgrade $http_upgrade` to forward WebSocket connections properly.


**Mobile network interruptions** — Mobile clients regularly switch networks (4G to Wi-Fi) or go through tunnels. The TCP connection drops, the client doesn't always get a clean close event, and the server might keep the connection in its table for a while. Heartbeat/ping messages are the solution — send a ping every 30 seconds, close the connection if no pong arrives within 10 seconds. The WebSocket spec has a built-in ping/pong frame type for exactly this.


**Authentication** — You can't set custom headers during the WebSocket handshake from a browser (unlike regular HTTP requests). The common patterns are: pass the token as a query parameter in the WebSocket URL (`wss://example.com/ws?token=abc`) — less clean since tokens in URLs end up in logs — or establish the connection and authenticate immediately via the first message. The query parameter approach is most common in practice despite the log concern.


For understanding the authentication patterns in more depth, the post on
OAuth 2.0 for developers covers the token flow that typically feeds into WebSocket authentication. If you're debugging JWT tokens that come back from your auth server, the JWT decoder at ToolsFuel lets you inspect the payload without leaving the browser.

Frequently Asked Questions

What's the difference between ws:// and wss://?

ws:// is unencrypted WebSocket — the equivalent of http://. wss:// is WebSocket over TLS — the equivalent of https://. In production, always use wss://. Unencrypted ws:// connections can be intercepted and modified by network intermediaries. Modern browsers also block ws:// connections from pages loaded over https:// (mixed content policy). In development on localhost, ws:// is fine since loopback traffic doesn't leave your machine.

How many WebSocket connections can a server handle?

More than you'd think. A single Node.js process can handle tens of thousands of concurrent WebSocket connections because WebSockets are I/O-bound rather than CPU-bound — the server isn't doing much work per connection, just maintaining the open socket. The actual limit depends on your OS file descriptor limit (each connection is a file descriptor), server RAM, and how much work your message handlers do. A well-tuned Node.js server can handle 100,000+ concurrent connections. The scaling challenge isn't usually raw connection count — it's horizontal scaling across multiple servers.

Can WebSockets work through firewalls and proxies?

Usually yes, because the WebSocket handshake starts as an HTTP request on port 80 (or 443 for wss://), which most firewalls allow. Some corporate firewalls or deep packet inspection setups that don't understand the HTTP Upgrade mechanism can block the connection upgrade. In practice, wss:// on port 443 has the highest compatibility since it looks like HTTPS traffic and passes through almost all firewalls. If you need maximum compatibility, Socket.IO's fallback to long polling handles environments where WebSockets are blocked.

Do WebSockets work in React Native or mobile apps?

Yes. React Native has a built-in WebSocket API with the same interface as the browser WebSocket API. Mobile-specific considerations: handle the app going to background (connections typically drop), handle network transitions between WiFi and cellular, and implement reconnection logic for when the app comes back to foreground. The react-native-websocket and socket.io-client libraries handle these mobile edge cases with less manual work than the native API. If you need to debug the JWT tokens used to authenticate WebSocket connections, the [ToolsFuel JWT decoder](/tools/jwt-decoder) lets you inspect token claims directly in the browser without any server calls.

What's Socket.IO and do I need it?

Socket.IO is a library that wraps WebSockets and adds features: automatic reconnection, rooms/namespaces for grouping connections, fallback to long polling when WebSockets aren't available, and a message acknowledgment system. You need it if you want those features without building them yourself. You don't need it if you're comfortable building reconnection logic and don't need rooms or acks — the raw WebSocket API is smaller and simpler. Socket.IO also requires the Socket.IO client library on the frontend — you can't connect a plain WebSocket client to a Socket.IO server.

When should I use Server-Sent Events instead of WebSockets?

Use Server-Sent Events (SSE) when data only flows one direction: server to client. SSE is simpler to implement (just a special HTTP endpoint), automatically reconnects on disconnect, works through HTTP/2 multiplexing, and doesn't require any extra libraries. Good use cases: live notifications, activity feeds, progress updates for long-running tasks, live sports scores. Use WebSockets when clients also need to send frequent data to the server — chat messages, game inputs, collaborative editing cursors.

Try ToolsFuel

23+ free online tools for developers, designers, and everyone. No signup required.

Browse All Tools