Tool

WebSocket test.

Open a real WebSocket connection from your browser, send messages, and read the response transcript with live byte counts and close-code labels. The interactive equivalent of wscat — useful for debugging realtime APIs without leaving the tab.

Status
disconnected
Sent / received
0 / 0
Up time

WebSocket URL
Public echo servers
Send message
Transcript (0)
— no messages yet · connect and send something —
Stats
Messages sent0
Messages received0
Bytes sent0 B
Bytes received0 B
Connected since

From HTTP to a persistent socket.

A WebSocket connection starts life as an ordinary HTTP request. The client sends a GET request with three special headers: Upgrade: websocket, Connection: Upgrade, and Sec-WebSocket-Key (a base64-encoded 16-byte random value). The server, if it accepts the upgrade, responds with HTTP 101 Switching Protocols and a Sec-WebSocket-Accept header that's a SHA-1 hash of the client's key concatenated with the magic GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. The hash is purely a sanity check — any HTTP server that doesn't speak WebSocket would not produce the correct hash, so the client knows the response really came from a WebSocket-aware peer.

Once the handshake completes, the TCP connection is reused for full-duplex framed messages. The HTTP layer is gone; the wire protocol is now WebSocket frames, defined in RFC 6455 (December 2011). Each frame has a small header (2–14 bytes) carrying type (text, binary, ping, pong, close), payload length, and a masking key for client-to-server frames. The framing is deliberately minimal — much smaller per-message overhead than HTTP/1.1 request/response cycles, which is why WebSocket beats long-polling for high-frequency message exchange.

The masking requirement is a defence against cache-poisoning attacks on intermediate proxies. Without masking, a malicious browser script could craft WebSocket frames that look like valid HTTP responses to a misconfigured proxy and corrupt its cache. The client masks every payload byte with a 32-bit XOR key (different per frame) so the bytes-on-the-wire don't resemble any other protocol. Server-to-client frames are unmasked because the server, by definition, isn't trying to abuse a proxy in the same way. The XOR is computationally trivial (one cycle per byte on modern CPUs) but catches attempts to smuggle protocol confusion.

In the browser, the entire handshake and framing protocol is hidden behind the WebSocket constructor. Open a connection with new WebSocket('wss://example.com/socket'); attach event listeners for open, message, error, close; call .send() with a string or binary payload. The browser handles the upgrade negotiation, the masking, the ping/pong keepalive frames, and the close handshake. This page's WebSocket Tester does exactly that — minimal layering on top of the browser's built-in implementation.

Decoding the four-digit goodbye.

When a WebSocket closes, both sides report a 16-bit code and an optional reason string. RFC 6455 defines specific ranges. 1000 is a normal closure — both sides agreed to disconnect. 1001 means the peer is going away (browser navigated, server shutting down). 1002 is a protocol error — usually a malformed frame; this almost always indicates a bug in one side's implementation. 1003 is unsupported data — typically a binary frame received by a server that only handles text, or vice versa.

1006 is the most common in practice and the most diagnostically useful. It means abnormal closure — the connection dropped without a close frame. Causes: network blip, NAT timeout (most home and corporate firewalls drop idle TCP connections after a few minutes), the peer process was killed without a clean shutdown, or a load-balancer reaped the connection after its idle limit. 1006 is what you see when a WebSocket "just dies" without warning. The fix is usually to add application-level keepalive — send a ping frame every 30–60 seconds — and to reconnect on close with exponential backoff.

1008 is policy violation — the server received a message that violates its rules. Common cases: rate limit exceeded, authentication failed mid-session, message too large. 1009 is the size-specific case (often used with a max-payload limit). 1011 is internal server error — the server failed mid-conversation and is hanging up. The codes from 4000–4999 are application-defined; protocols layered on WebSocket (Phoenix Channels, Discord's gateway, Bitfinex's WS API) frequently use these for protocol-specific signalling.

An undocumented but common pitfall: browsers report close code 1006 for many cases that the server actually closed with a different code. The reason is that until the close handshake completes (server sends close frame, client acknowledges), the browser doesn't know the server's chosen code. If the TCP connection drops before that handshake finishes, the browser falls back to 1006. To get the real code, the server must send a close frame and wait for the client's acknowledgement before tearing down the TCP connection — most server libraries handle this correctly when configured.

When the client can't keep up.

WebSocket has no built-in flow control above the TCP layer. If a server sends messages faster than the client can process them, the messages queue in the operating system's TCP receive buffer; once that fills, TCP's congestion control kicks in and the server's .send() calls block (in synchronous frameworks) or back-pressure (in async frameworks). The server can't tell the difference between "client is slow" and "network is congested" from the WebSocket layer alone.

Production WebSocket APIs solve this in one of three ways. The first is application-level acknowledgements: the server sends a batch of messages, then waits for an {type:"ack",seq:N} from the client before sending the next batch. This adds round-trip latency but prevents the client from drowning. Phoenix Channels uses this pattern via its push/reply primitives; Apache Pulsar's WebSocket gateway works similarly.

The second is server-side credit accounting: the server tracks how many messages each client has consumed (via explicit acks) and pauses transmission once a sliding window of unacknowledged messages exceeds a threshold. This is similar to TCP's congestion window and gives finer-grained control than batch-ack. AWS API Gateway WebSockets, Slack's RTM API, and Discord's Gateway all do credit-based throttling.

The third is to drop messages. For ephemeral data — live cursor positions, real-time chart updates, presence indicators — the server can simply discard messages that the client hasn't acked within a short window. The client is always seeing only the latest state. This is what most multiplayer game protocols do, where stale position updates are useless and dropping them is preferable to delivering them late.

The browser's WebSocket API exposes bufferedAmount — the number of bytes queued for transmission but not yet sent. Production client code checks this before each .send(); if it exceeds a threshold (say, 1 MB), pause sending and wait for the buffer to drain. This is the WebSocket equivalent of TCP's window-based flow control, exposed at the application layer.

Three cases where SSE or HTTP/2 wins.

WebSocket is the right tool for bidirectional, low-latency message exchange — chat, multiplayer games, collaborative editing, live trading dashboards. For streaming server data to a client without much client-to-server traffic, Server-Sent Events (SSE) are simpler and often better. SSE is just an HTTP GET that never closes; the server sends data: ...\n\n blocks as events arrive. No upgrade handshake, no masking, no framing layer. Browsers reconnect automatically with last-event-ID resumption. Reverse proxies, CDNs, and load balancers handle SSE as ordinary HTTP/1.1 connections — no special configuration needed. WebSocket frequently breaks at intermediate hops; SSE rarely does.

For request/response patterns where you only occasionally need server push, HTTP/2 server push and HTTP/3's QUIC streams are increasingly the right answer. They preserve the request/response model while letting the server push related resources without a round trip. Modern frameworks (Next.js streaming, SvelteKit's data loaders) lean on this rather than on WebSocket. If your "real-time" use case is "send updates to the client every few seconds", neither WebSocket nor SSE — long-polling with a 30-second timeout works fine.

For data that needs ordering and durability — events that must not be lost across a reconnect — neither WebSocket nor SSE is sufficient. You need a message broker (Kafka, NATS, Pulsar) with explicit acknowledgement and replay. WebSocket is the wire transport between your service and the client; the broker is what makes the events durable. Building "WebSocket as the source of truth" is a common architectural mistake; the right shape is "WebSocket carries deltas, broker keeps the canonical sequence, reconnects replay missed deltas via REST or broker query".

Found this useful?