How to test websocket connection
Connect with a real client and print what it reports. Node 22 has a built-in WebSocket, so node -e needs no dependency: readyState goes 0 to 1, the server echoes the frame you send, and close arrives with code 1000. Any other sequence names the failure.
Why check this
Run this on every deploy that touches the socket path, and first on any environment where the front end reports a connection problem. A tester who only watches the browser sees a spinner and an empty console. A tester who runs a client sees the state machine, and the state machine says which half of the connection broke.
The failure this catches is the silent one. The socket opens, the server accepts it, and no frame ever comes back because the handler was registered on a different path or a different event name. Both sides look connected, the feature does nothing, and nothing is logged as an error. Sending one frame and waiting for the echo settles it in under a second.
Prerequisites
- Node 22. The global
WebSocketclient has been available without a flag since Node 22, so nothing needs installing for the client side. This page used 22.23.2. - wscat 6 through
npx wscat@6, for a session you can type into. This page used 6.1.0. - An endpoint. The captures below run against a local echo server, so no third-party host is touched. Start it in an empty folder after
npm i ws@8:
// echo-server.mjs node 22, npm i ws@8
import { createServer } from 'node:http';
import { WebSocketServer } from 'ws';
const server = createServer((req, res) => {
res.writeHead(426, { 'content-type': 'text/plain', 'sec-websocket-version': '13' });
res.end('this endpoint speaks WebSocket only\n');
});
const wss = new WebSocketServer({
server,
path: '/ws',
verifyClient: ({ origin }) => origin === undefined || origin === 'https://app.example.com',
});
wss.on('connection', (ws, req) => {
console.log('open key=%s origin=%s', req.headers['sec-websocket-key'], req.headers.origin ?? '(none)');
ws.on('message', (data) => ws.send('echo:' + data));
});
server.listen(19317, '127.0.0.1', () => console.log('ws://127.0.0.1:19317/ws'));
Steps
- Step 1.
Connect, send one frame, and print every state change. Replace the URL with your endpoint.
node -e ' const ws = new WebSocket(process.argv[1]); console.log("new: readyState", ws.readyState); ws.onopen = () => { console.log("open: readyState", ws.readyState); ws.send("ping-1"); }; ws.onmessage = (e) => { console.log("message:", e.data); ws.close(1000, "done"); }; ws.onerror = (e) => console.log("error:", e.message); ws.onclose = (e) => console.log("close: code", e.code, "wasClean", e.wasClean); setTimeout(() => { console.log("after 3s: readyState", ws.readyState); process.exit(0); }, 3000); ' ws://127.0.0.1:19317/wsnew: readyState 0 open: readyState 1 message: echo:ping-1 close: code 1000 wasClean true after 3s: readyState 3That is a whole healthy session in five lines: CONNECTING (0), OPEN (1), a frame back from the server, a clean close with code 1000, CLOSED (3). Keep this command. Every step below changes only the URL.
- Step 2.
Open a session you can type into. The
sleep 4 |is not decoration, and the pitfalls below say why.sleep 4 | npx wscat@6 -c ws://127.0.0.1:19317/ws -x 'hello from wscat' -w 2echo:hello from wscat-xsends one message after the socket opens and-w 2keeps it open for two seconds afterwards. Without a terminal, wscat prints incoming frames only, with no<prefix and no connection banner. - Step 3.
Point the same client at a port where nothing listens, so you know what a refused connection looks like.
node -e ' const ws = new WebSocket(process.argv[1]); console.log("new: readyState", ws.readyState); ws.onopen = () => { console.log("open: readyState", ws.readyState); ws.send("ping-1"); }; ws.onmessage = (e) => { console.log("message:", e.data); ws.close(1000, "done"); }; ws.onerror = (e) => console.log("error:", e.message); ws.onclose = (e) => console.log("close: code", e.code, "wasClean", e.wasClean); setTimeout(() => { console.log("after 3s: readyState", ws.readyState); process.exit(0); }, 3000); ' ws://127.0.0.1:19399/wsnew: readyState 0 error: Received network error or non-101 status code. after 3s: readyState 0Two things here are worth more than the error text. No
closeline appeared, and readyState is still 0 three seconds later, not 3. - Step 4.
Point it at a path the server answers with 400, and compare.
node -e ' const ws = new WebSocket(process.argv[1]); console.log("new: readyState", ws.readyState); ws.onopen = () => { console.log("open: readyState", ws.readyState); ws.send("ping-1"); }; ws.onmessage = (e) => { console.log("message:", e.data); ws.close(1000, "done"); }; ws.onerror = (e) => console.log("error:", e.message); ws.onclose = (e) => console.log("close: code", e.code, "wasClean", e.wasClean); setTimeout(() => { console.log("after 3s: readyState", ws.readyState); process.exit(0); }, 3000); ' ws://127.0.0.1:19317/socketnew: readyState 0 error: Received network error or non-101 status code. after 3s: readyState 0Byte for byte the same as step 3, and the causes could not be further apart. A dead port and a server that answered an HTTP error are one message to this client.
- Step 5.
Run the failing URL through wscat, which reports the status code the server sent.
sleep 3 | npx wscat@6 -c ws://127.0.0.1:19317/socketerror: Unexpected server response: 400Same URL, same failure, a usable message. When a client library says nothing, change clients before changing code.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| open: readyState 1 then a message: line | The socket works end to end | Nothing. This is the shape you want. |
| open: readyState 1 and no message | The handshake passed, the handler did not fire | Check the server's message handler and the path it is bound to |
| error and readyState stuck at 0 | The connection never opened | Rerun with wscat for the status code |
| error: Unexpected server response: 400 | The server refused the upgrade | Read the response body with curl |
| close: code 1006 with no error | The peer vanished without a close frame | Look at the proxy idle timeout, not at the application |
| wscat prints nothing and exits 0 | Nothing held stdin open | Pipe something into it, as step 2 does |
Common mistakes
What to check next
- Websocket handshake failed: read the HTTP response behind the error messages above.
- How to test websocket with curl: a third client, and the only one that shows the raw upgrade.
- Websocket close code 1006: what the close code says about who dropped the socket.
- How to test websocket reconnect: the retry loop the second pitfall breaks.
- How to check websocket connection in chrome: the same session viewed as frames in DevTools.
FAQ
How to check websocket connection status?
Read readyState: 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED. Print it after your own timeout, as step 1 does. A value of 0 after three seconds is a connection that never opened, not one still trying.
How to test a websocket server without writing code?
Use wscat: npx wscat@6 -c ws://host/path. It opens a prompt, sends what you type, and prints what arrives. In a script, add -x to send one message and hold stdin open so it lives long enough to print the reply.
Does a 101 in the Network tab mean the connection works?
No. It means the handshake finished. Send a frame and wait for one back before calling the socket healthy.
Which client should I trust when two disagree?
The one that shows the HTTP response. wscat reports the status code and curl reports the status, headers and body. A library that reports a network error has told you only that it did not open.
Can I test a wss endpoint the same way?
Yes. Change the scheme to wss://. Certificate verification then applies, so a self-signed certificate fails until you trust it or pass -n to wscat.
Verified
Verified by Maks Vernynode 22.23.2ws 8.21.3wscat 6.1.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic6 minpublished updated Maks Verny