To debug WebSocket connections you inspect two things: the HTTP upgrade handshake that opens the channel, and every frame that flows over it afterward. Unlike a request/response pair, a WebSocket is a long-lived, full-duplex connection, so the useful signal is spread across many small messages, control frames, and a final close code. The workflow below covers how the protocol works on the wire, the frame and close-code details that explain real failures, and how to read them in practice.
What to inspect when you debug a WebSocket connection
A complete WebSocket debugging pass looks at four layers, in order:
- The handshake - the
GETrequest withUpgrade: websocketand the server's101 Switching Protocolsresponse. If this fails, no frames are ever exchanged. - Data frames - the
TextandBinarymessages your application actually sends and receives, plus their direction and timing. - Control frames -
Ping/Pongkeepalives and theCloseframe, which explain idle drops and disconnects. - The close code - the numeric reason the connection ended, which is often the single most useful clue.
How the WebSocket protocol works under the hood
Most connection bugs are easier to read once you know what the bytes mean. WebSocket is defined by RFC 6455, and compression by RFC 7692.
The upgrade handshake
A WebSocket starts life as an ordinary HTTP/1.1 request. The client sends
an Upgrade request with a random
Sec-WebSocket-Key; the server confirms with
101 Switching Protocols and a matching
Sec-WebSocket-Accept. The accept value is not a secret - it
is the SHA-1 hash of the client key concatenated with the fixed GUID
258EAFA5-E914-47DA-95CA-C5AB0DC85B11, Base64-encoded. A
mismatched or missing accept header means a proxy or server mangled the
handshake.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: permessage-deflate
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The same handshake also carries the negotiated subprotocol
(Sec-WebSocket-Protocol) and extensions
(Sec-WebSocket-Extensions). Reading these
HTTP headers tells you whether
compression was actually agreed, not just requested.
Frame anatomy and opcodes
After the upgrade, data travels in frames rather than messages. Each frame
begins with a FIN bit (is this the last fragment?), three
reserved bits - RSV1 doubles as the per-message-deflate flag -
a 4-bit opcode, a MASK bit, and a payload length encoded in 7,
7+16, or 7+64 bits. The opcode decides how to read the rest:
| Opcode | Type | Purpose |
|---|---|---|
0x0 | Continuation | Next fragment of a split message |
0x1 | Text | UTF-8 text payload |
0x2 | Binary | Raw binary payload |
0x8 | Close | Closing handshake, carries a close code |
0x9 | Ping | Keepalive probe |
0xA | Pong | Reply to a ping |
Two details trip people up. First, every client-to-server frame is
masked: the payload is XOR-ed with a 4-byte key, so a raw
capture of an outgoing frame looks like noise until it is unmasked.
Server-to-client frames are never masked. Second, a single logical message
can be fragmented across one initial frame
(FIN=0) and any number of Continuation frames,
which is why a payload may appear to arrive in pieces.
Control frames and compression
Ping and Pong frames keep an otherwise idle
connection alive; if a load balancer or proxy times out before the next
ping, you get an abnormal close. A Close frame carries a
2-byte close code followed by an optional UTF-8 reason string. When
permessage-deflate is negotiated, text and binary payloads are
DEFLATE-compressed and the RSV1 bit is set; the
client_no_context_takeover and
server_no_context_takeover parameters control whether the
compression window resets between messages. A capture that does not inflate
these frames shows compressed bytes instead of your JSON.
WebSocket close codes worth knowing
The close code is defined in RFC 6455 §7.4. These are the ones you will see most while debugging:
| Code | Name | What it usually means |
|---|---|---|
| 1000 | Normal Closure | Connection finished cleanly |
| 1001 | Going Away | Server shutting down or client navigated away |
| 1002 | Protocol Error | A frame violated the protocol |
| 1006 | Abnormal Closure | No close frame received - network drop, crash, or proxy timeout |
| 1009 | Message Too Big | Payload exceeded the peer's limit |
| 1011 | Internal Error | Unhandled exception on the server |
| 1015 | TLS Handshake | wss:// TLS negotiation failed |
Codes 1005, 1006, and 1015 are never sent on the wire -
the browser or runtime sets them locally when no real close frame arrived.
That is why a 1006 never has a reason string: the connection
died before a Close frame could be exchanged. To find the
cause, you have to look one layer down at the handshake, the last frames
before the drop, and the keepalive timing - not at the close event itself.
How to debug WebSocket connections step by step
- Confirm the handshake.
Verify the request carries
Upgrade: websocketand the response is101 Switching Protocolswith a validSec-WebSocket-Accept. A200,301, or403here means the upgrade was blocked or rewritten upstream. - Read the frames in order. Follow sent and received frames on a single timeline. Check opcodes and direction so you can see which side stopped talking and when.
- Decode the payload.
Inflate
permessage-deflateframes and pretty-print JSON so you compare application data, not compressed bytes. Confirm text frames are valid UTF-8. - Check keepalive and close.
Look at
Ping/Pongspacing for idle drops, and read theClosecode and reason to separate a clean shutdown from a network or server failure.
In the browser, Chrome and Firefox DevTools expose this under the Network tab's WS filter, with a Messages view of frames. That works for pages you load yourself. It does not help when the WebSocket lives in a desktop app, a background service, a CLI tool, a mobile emulator, or a server-to-server call - and it cannot show you a connection that opened before DevTools was attached.
Debugging WebSocket frames in HTTP Debugger
HTTP Debugger Pro captures WebSocket traffic system-wide on Windows
without a proxy or browser extension, so it sees connections from any
process - browsers, desktop apps, services, and CLI tools - including
encrypted wss:// and localhost. Because capture is not tied
to a browser tab, it also catches connections that were already open
before you started looking.
Each WebSocket connection gets its own Messages view: a single timeline of sent and received frames with direction, opcode type, byte size, and a payload preview. You can filter by frame type or direction and search across payloads.
The Info tab decodes each frame down to the wire:
opcode, FIN/final flag, fragmentation, masking, and the
compression state. This is where the
permessage-deflate details show up - whether the payload was
compressed and successfully decoded - so a frame that looks like garbage
in a raw capture reads as plain text here.
For Text frames, the payload is shown as readable text and
auto-formatted when it is JSON, with Raw and Hex views alongside.
Close frames are parsed into their close code and reason,
and Ping/Pong frames appear inline so idle
disconnects are easy to trace. WebSocket traffic is read-only here -
HTTP Debugger inspects and decodes frames rather than injecting or
rewriting them.
It complements browser tooling: use DevTools for a page you control, and a system-wide HTTP sniffer when the WebSocket lives outside the browser or rides HTTP traffic you cannot attach DevTools to. The same connection's upgrade headers and frames sit next to the rest of your captured session, which makes it a practical HTTP analyzer for real-time protocols.
Common mistakes when debugging WebSockets
- Treating 1006 as the cause. Code 1006 only says the connection dropped without a close frame. The real reason is in the handshake, the proxy timeout, or the last frames before the drop.
- Reading compressed frames raw. If
permessage-deflatewas negotiated, an un-inflated payload looks like binary noise. Decode it before assuming the data is corrupt. - Forgetting masking. Outgoing client frames are XOR-masked on the wire, so a raw byte dump of a sent message is not your payload until it is unmasked.
- Ignoring ping/pong timing. Many "random" disconnects are idle timeouts at a proxy or load balancer between keepalives, not application bugs.
- Debugging only in the browser. DevTools cannot see WebSockets from desktop apps, services, or connections opened before it attached.
FAQ
How do I see the messages in a WebSocket connection?
Open a frame timeline for the connection - the Messages view in DevTools, or the Messages view in a system-wide capture tool - and read sent and received frames in order, with their opcode, direction, size, and decoded payload.
Why does my WebSocket close with code 1006?
1006 means the connection closed abnormally without a close frame, usually a network drop, a server crash, or a proxy idle timeout. Because it is set locally by the runtime, it never carries a reason - diagnose it from the handshake, keepalive timing, and the frames before the drop.
Can I debug secure wss:// WebSocket traffic?
Yes. After the TLS layer is decrypted - via the browser's TLS key log for packet captures, or a local root certificate in HTTP Debugger - wss:// frames are inspected exactly like plaintext ws:// frames.
Can I debug WebSockets outside the browser?
Browser DevTools only sees WebSockets from pages it is attached to. For desktop apps, background services, CLI tools, or emulators, use a system-wide capture tool such as HTTP Debugger Pro that intercepts traffic from any process on the machine.
Why does my WebSocket payload look like binary garbage?
The connection most likely negotiated permessage-deflate, so frames are DEFLATE-compressed with the RSV1 bit set. Inflate the payload before reading it; a tool that decodes compression shows the original text or JSON instead of compressed bytes.