WebSocket Closed With Code 1006 (tested)

1006 is the WebSocket close code a client makes up when the connection dropped without a proper close handshake. It is never sent on the wire. We reproduced it with a real Node.js ws server and two real clients (Chromium and a Node ws client), and contrasted it with a clean 1000 close and a server-side 1011 error close.

1. What 1006 means

Every WebSocket close normally carries a close frame with a code and an optional reason, sent by whichever side closes first. 1006 is different: the spec reserves it as a code the implementation reports locally when the connection was lost without a close frame. You will never see 1006 arrive on the wire, and you can never set it yourself with ws.close(1006) — browsers reject that call.

In practice 1006 means: the TCP connection ended, or stopped responding, before either side sent a close frame. The client's WebSocket.onclose fires with code: 1006, reason: "", wasClean: false.

2. Our test setup

A Node.js ws server on 127.0.0.1:8990 exposed four paths, one per scenario:

wss.on('connection', (ws, req) => {
  if (req.url === '/clean')       setTimeout(() => ws.close(1000, 'bye'), 200);
  if (req.url === '/servererror') setTimeout(() => ws.close(1011, 'internal error'), 200);
  if (req.url === '/destroy')     setTimeout(() => ws._socket.destroy(), 200);
  if (req.url === '/abrupt')      { /* process gets kill -9'd from outside */ }
});

We connected with a real browser (Chromium 153, via Playwright) and separately with a Node ws client, and read the actual CloseEvent/close callback.

3. Results: four closes, four different codes

ScenarioChromium oncloseNode ws client close
ws.close(1000, 'bye') — clean closecode 1000, reason "bye", wasClean truesame
ws.close(1011, 'internal error') — server-side error closecode 1011, reason "internal error", wasClean truesame
ws._socket.destroy() — raw TCP socket cut, process stays alive, no close framecode 1006, reason "", wasClean falsecode 1006, reason ""
kill -9 on the whole server process while a client was connectedcode 1006, reason "", wasClean false, observed 19–466 ms after the killcode 1006, reason ""

Both a mid-stream socket.destroy() and a full kill -9 of the server process produced the same result on both clients: 1006, empty reason, wasClean: false. A clean ws.close(1000) and a deliberate server error close with ws.close(1011, ...) both arrived with wasClean: true and the exact code and reason string the server sent. Chromium did not fire a separate onerror event before onclose in the 1006 cases in our runs — only onclose fired, with 1006.

4. Why 1006 happens

In all of these, nobody explicitly decided to close the WebSocket with a code; the code is the client's own inference that the handshake never completed.

5. How to tell 1006 apart from a real close code

Log the full event, not just a truthy/falsy check:

ws.onclose = (e) => {
  console.log('code', e.code, 'reason', JSON.stringify(e.reason), 'wasClean', e.wasClean);
};

If wasClean is false and code is 1006, you lost the transport, not the application. Codes like 1000 (normal), 1001 (going away, e.g. tab closed or server shutting down gracefully), and 1011 (server hit an internal error) all arrive with wasClean: true because a real close frame was exchanged, as our tests confirm.

6. Fixes and mitigations

Frequently asked questions

What does WebSocket close code 1006 mean?

It means the connection was lost without a close frame being exchanged. In our tests, both killing the server process and cutting the TCP socket produced code 1006 with an empty reason and wasClean false, on both a browser client and a Node ws client.

Can a server send code 1006 on purpose?

No. We confirmed browsers reject ws.close(1006) outright; it is reserved for the client or server implementation to report locally when no close frame arrived, not a code either side transmits.

How is 1006 different from 1000 or 1011?

In our tests, ws.close(1000, 'bye') and ws.close(1011, 'internal error') both arrived with wasClean true and the exact reason string sent. 1006 arrived with wasClean false and an empty reason, because no close frame was ever sent.

Does 1006 mean my server crashed?

It's one cause, and the one we measured with kill -9. It can equally mean a proxy or NAT dropped an idle connection, or the client's own network dropped out. The code alone can't tell you which; check server logs and timing.

What should my client do when it gets code 1006?

Reconnect with backoff. Since 1006 carries no information beyond 'transport gone', retrying is the standard response. Pair it with a ping/pong heartbeat so real timeouts get detected and closed with a proper code instead of silently becoming a 1006.

Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0 (ws 8.x server and client) and headless Chromium 153 via Playwright, against a local ws server on 127.0.0.1:8990. Each close code below is a close code we actually received, not a description from a spec. PlainHeaders is our extension; Node.js, ws and Chromium are free.