Inspecting WebSocket frames in DevTools and Wireshark #
The client says it sent the message; the server says it never arrived. Or the connection closes with 1006 and no one knows which side gave up. Or a proxy in the middle is suspected of mangling traffic. Real-time bugs are hard to reason about from logs alone, because the interesting facts live in the frames: what was sent, in what order, with which opcode, how it was fragmented, and who sent the close frame with which code. Browsers and packet analyzers can show every one of those details, including for encrypted wss:// traffic, if you know where to look. This page is a practical tour of the tools, from the quickest look in DevTools to decrypted packet captures.
Root cause #
WebSocket traffic is invisible to most of the tools engineers reach for first. HTTP access logs record the upgrade request and nothing after it. APM tools trace the handshake and then lose the connection. Server logs show what your code chose to log, which after a bug is rarely the frame that mattered. And on the wire the traffic is TLS-encrypted, so a naive packet capture shows only sizes and timing.
What you actually need to see falls into three layers. The message layer: which application messages were exchanged, in which direction, when — DevTools shows this. The frame layer: opcodes, fragmentation, ping/pong, close codes, masking, compression — Wireshark decodes this. The transport layer: TCP resets, retransmissions, TLS alerts, who closed the connection — also Wireshark, and ss on the server.
Resolution #
DevTools: the message view #
In Chrome and Edge, open the Network panel, filter by WS, reload, and select the connection. The Messages tab lists every message with direction arrows (up for sent, down for received), size and timestamp; binary messages show as hex or base64. Right-click to copy a message, and use the filter box to search payloads. The Headers tab shows the upgrade request and the 101 response, including negotiated subprotocol and extensions such as permessage-deflate. When the connection ends, the last entry shows the close with code and reason if one was received.
Firefox’s Network panel offers the same Response view for WebSockets with message filtering by type (text, binary, control frames if enabled in settings) and a useful toggle to show ping/pong control frames, which Chrome hides.
A quick console-side complement is to wrap the socket so every message is logged with a sequence number, which makes ordering questions easy to answer:
// Development-only: log every frame the page sends or receives, with direction and timing.
export function traceSocket(ws: WebSocket, label = 'ws') {
const t0 = performance.now();
let n = 0;
const stamp = () => `${label} #${++n} +${Math.round(performance.now() - t0)}ms`;
const origSend = ws.send.bind(ws);
ws.send = (data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
console.debug(`${stamp()} ↑`, typeof data === 'string' ? data.slice(0, 500) : data);
origSend(data);
};
ws.addEventListener('message', (e) => console.debug(`${stamp()} ↓`, typeof e.data === 'string' ? e.data.slice(0, 500) : e.data));
ws.addEventListener('close', (e) => console.debug(`${stamp()} close`, e.code, e.reason, 'clean:', e.wasClean));
ws.addEventListener('error', () => console.debug(`${stamp()} error`));
}
wasClean on the close event is worth logging: false means no close frame was exchanged, which points at the network or a crashed peer rather than a deliberate close — the distinction behind code 1006 in WebSocket close codes explained.
Wireshark: decrypting wss:// traffic #
To see frames on the wire, capture the traffic and give Wireshark the TLS session keys. Chrome, Firefox and Node.js all write keys to a file named by the SSLKEYLOGFILE environment variable:
# 1. Start the browser (or Node) with a key log file.
export SSLKEYLOGFILE="$HOME/tls-keys.log"
google-chrome --user-data-dir=/tmp/chrome-debug & # or: node --tls-keylog=$HOME/tls-keys.log server.js
# 2. Capture the traffic for your host.
sudo tcpdump -i any -w ws.pcap 'host rt.example.com and port 443'
# 3. Open ws.pcap in Wireshark; set Preferences → Protocols → TLS → (Pre)-Master-Secret log filename
# to ~/tls-keys.log. Then filter:
# websocket all WebSocket frames
# websocket.opcode == 8 close frames (payload shows the status code)
# websocket.opcode == 9 || websocket.opcode == 10 ping / pong
# websocket.fin == 0 fragmented messages (continuation follows)
# tcp.flags.reset == 1 who reset the connection
With keys loaded, Wireshark decodes each frame: FIN bit, opcode (1 text, 2 binary, 0 continuation, 8 close, 9 ping, 10 pong), mask bit and key (set on every client-to-server frame), payload length and — when permessage-deflate is negotiated — the decompressed payload. Following a TCP stream with Follow → WebSocket Stream reconstructs the conversation.
Treat the key log file like a password: anyone with it and a capture can read the traffic. Use a throwaway browser profile, delete the file afterwards, and never enable key logging in production processes.
Edge cases #
Compression hides payloads. With permessage-deflate, raw captures show compressed bytes; Wireshark decompresses only if it saw the whole connection from the handshake, because the compression context spans messages. Capture from before the connection opens.
HTTP/2 WebSockets. When the browser carries the WebSocket over HTTP/2 (RFC 8441), Wireshark shows it as DATA frames on an HTTP/2 stream, and DevTools lists the protocol as h2. See the WebSocket handshake over HTTP/2.
Capturing on the right side of the proxy. A capture on the client shows the client-to-proxy leg; a capture on the server shows proxy-to-server. When a proxy is suspected, capture both and compare: a message present on one side and missing on the other locates the culprit.
Verification #
Practise on a known-good connection before you need it in an incident. Open a test page, send a text message, a binary message and a large message that the client library fragments, wait for a server ping, then close with a custom code:
const ws = new WebSocket('wss://rt.example.com/ws');
traceSocket(ws, 'probe');
ws.onopen = () => {
ws.send('hello'); // opcode 1
ws.send(new Uint8Array([1, 2, 3])); // opcode 2
ws.send('x'.repeat(200_000)); // may appear as several frames on the wire
setTimeout(() => ws.close(4000, 'inspection done'), 35_000); // after at least one server ping
};
In DevTools you should see four messages and the close; in Wireshark, the same messages as frames with opcodes 1 and 2, a ping/pong pair, and a close frame whose payload decodes to 4000 and “inspection done”. Once the pattern is familiar, incident captures read quickly.
Operational checklist #
FAQ #
How do I see WebSocket messages in Chrome? #
Open DevTools, go to the Network panel, filter by WS, select the connection and open the Messages tab. It lists every message sent and received with direction, size and timestamp.
Can Wireshark decrypt wss:// traffic? #
Yes, if you provide the TLS session keys. Launch the browser or Node process with SSLKEYLOGFILE set, then point Wireshark’s TLS preferences at that file. Without keys, only sizes and timing are visible.
Why don’t I see ping and pong frames in DevTools? #
Chrome’s message view hides control frames. Firefox can show them if enabled in the network panel’s WebSocket settings, and Wireshark always shows them. Browsers answer pings automatically, so the page’s JavaScript never sees them.
How do I tell who closed a WebSocket? #
Find the first close frame (opcode 8) or TCP reset in a capture and note its direction. A close frame from the server with a code is deliberate; a reset from an intermediate address, with no close frame, points at a proxy or network device.
Related #
- Debugging WebSocket Handshake Failures — when the connection never opens.
- Fragmented WebSocket Frames and Max Payload — what continuation frames mean.
- WebSocket Close Codes Explained — interpreting the close frames you capture.
- Structured Logging for WebSocket Connections — making captures rarely necessary.
Back to Protocol Handshake Mechanics.