Field guide
How to read socket.io messages in Chrome DevTools
Chrome shows you every socket.io frame your app exchanges. It just shows them as the wire carries them: 42["chat message",{…}], a bare 2 every 25 seconds, and Binary Message (214 bytes) where your payload used to be. Here's how to read that by hand — and where hand-reading stops working.
Finding the frames
Open DevTools, go to Network, click the WS filter, select the connection, then the Messages tab. Green arrows are frames you sent, red are frames you received, and each row carries a timestamp and a length.
Two things to know before you trust that list:
- It starts recording when you open DevTools. The connection handshake — and everything your app did before you pressed F12 — is not there. Reload the page with DevTools already open, or you're reading the middle of a conversation.
- socket.io may not be on WebSocket at all. By default it connects over HTTP long-polling first and upgrades to WebSocket afterwards, so early traffic hides under XHR requests to
/socket.io/?EIO=4&transport=polling.
Two protocols, stacked
What confuses most people is that a socket.io frame is two protocols in a trench coat. Engine.IO handles the connection — handshake, heartbeat, transport upgrade — and socket.io rides inside its message packets, carrying your events.
So the leading digits are not part of your data. They're framing, and each layer contributes one.
| Digit | Type | What it means when you see it |
|---|---|---|
| 0 | open | The handshake. Payload is JSON with sid, upgrades, pingInterval, pingTimeout, maxPayload. |
| 1 | close | Transport is closing. |
| 2 | ping | Heartbeat. In protocol v4 the server sends these, on the pingInterval from the handshake. |
| 3 | pong | The client's answer. If it doesn't arrive within pingTimeout, the server drops the connection. |
| 4 | message | Everything of yours. The rest of the frame is a socket.io packet. |
| 5 | upgrade | Polling has been upgraded to WebSocket. |
| 6 | noop | Filler, used to wake up a hanging poll. |
| Digit | Type | What it means when you see it |
|---|---|---|
| 0 | CONNECT | Namespace connection established. |
| 1 | DISCONNECT | Leaving a namespace. |
| 2 | EVENT | The one you care about: ["eventName", …args]. |
| 3 | ACK | The callback firing for an event you acknowledged. |
| 4 | CONNECT_ERROR | Rejected — usually auth middleware. |
| 5 | BINARY_EVENT | An event with binary attachments sent as separate frames. |
| 6 | BINARY_ACK | Same, for an ack. |
Decoding a frame by hand
The full socket.io encoding is:
<packet type>[<# of binary attachments>-][<namespace>,][<ack id>][JSON payload]
Everything in brackets is optional, and the namespace only appears when it isn't the default /. Wrapped in engine.io's 4, that gives you the frames you're actually staring at:
// engine.io handshake — the server opening the conversation
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000}
// socket.io CONNECT on the default namespace: engine.io 4 + socket.io 0
40
// CONNECT on /admin, with the session id assigned to that namespace
40/admin,{"sid":"oSO0OpakMV_3jnilAAAA"}
// an EVENT: engine.io 4 + socket.io 2, then the array
42["chat message",{"user":"ana","text":"hi"}]
// the same EVENT, on /admin, expecting ack 12
42/admin,12["chat message",{…}]
// the ACK coming back for id 12
4312[{"ok":true}]
// a BINARY_EVENT with one attachment; the bytes arrive as the NEXT frame
451-["upload",{"_placeholder":true,"num":0}]
// heartbeat: server ping, client pong. Not your app. Ignore these.
2
3
Read it left to right and it collapses quickly. 42 is “a message, and it's an event”. 4312 is “a message, an ack, for id 12”. The bare 2 arriving every 25 seconds like clockwork is the server checking you're alive.
The one-line version
Strip the leading 4. If the next digit is 2, the rest is ["event", payload] — that's your message. Anything else is plumbing.
Where hand-reading stops
The digits are easy. These four are the ones that actually cost you an afternoon:
1. The frames you already missed
DevTools captures from the moment it opens. If the bug happens during connection — a failed auth handshake, a CONNECT_ERROR, an event fired on page load — you have to reproduce it with DevTools already open, and hope the bug is reproducible on demand.
2. Binary frames
A binary payload shows as Binary Message (n bytes). If your stack sends MessagePack, CBOR, protobuf or a custom struct, that row tells you a message happened and nothing about what it said.
3. JSON that's been through a laundry cycle
Plenty of production stacks nest their real payload inside a string field, then base64 it, then compress it. A single frame can be JSON → string field → base64 → gzip → more JSON. DevTools shows you the outermost layer, correctly escaped, and unhelpfully.
// what you see
42["sync",{"d":"H4sIAAAAAAAAA6tWKkotLsjPS1WyUlAqSy0qzszPU9JRSkksSVSyqlbKTS0uTkxPVbIC…"}]
// what it says once you base64-decode and gunzip the field
{"cursor":"8172","rows":[{"id":41,"price":"1.85"},…]}
4. Frames from iframes
If the socket lives inside an embedded widget, you're now hunting through a different execution context in a list that doesn't tell you which frame each row came from. That one has company: see the seven reasons frames don't show up in DevTools at all.
The manual workarounds
Before reaching for a tool, these two get you surprisingly far:
- Client-side debug logging. socket.io ships with the
debugmodule: runlocalStorage.debug = 'socket.io-client:socket'in the console and reload. You get decoded events in the console, from the client's own parser — no protocol digits involved. It won't show you engine.io framing or anything the client discarded, but for “which events am I receiving” it's two seconds of work. - Patch the socket in the console. If you can reach the instance,
socket.onAny((event, ...args) => console.log(event, args))logs every inbound event. Only works after the socket exists, and only if it's reachable from the page's scope.
Both give you the client's view after parsing. Neither shows you the raw wire, which is exactly what you need when the bug is that the client parsed something differently than you expected.
Or read them already decoded
Wirepeek is a free Chrome DevTools panel built for this: it hooks WebSocket.prototype before socket.io grabs the native reference, so it captures from the handshake onward and survives reloads. socket.io and engine.io framing is resolved into event names and JSON, binary frames go through MessagePack, CBOR and Thrift decoders, and nested base64 or gzip fields get unwrapped in place. Read-only, no account, and nothing leaves your machine.
Quick reference
| Frame | Reading |
|---|---|
| 0{…} | engine.io handshake, carries sid and the heartbeat intervals |
| 40 | socket.io connected, default namespace |
| 42[…] | your event, as ["name", …args] |
| 42/ns,[…] | your event, on namespace /ns |
| 43<id>[…] | ack for the event with that id |
| 44{…} | connect error — read the payload, it's usually the auth message |
| 2 / 3 | heartbeat, server ping and client pong |
| 41 | namespace disconnect |
Protocol details here follow the socket.io protocol spec and the engine.io protocol spec (v4, the default since socket.io v3).