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

// 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

  1. 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/ws
    
    new: readyState 0
    open: readyState 1
    message: echo:ping-1
    close: code 1000 wasClean true
    after 3s: readyState 3

    That 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.

  2. 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 2
    
    echo:hello from wscat

    -x sends one message after the socket opens and -w 2 keeps it open for two seconds afterwards. Without a terminal, wscat prints incoming frames only, with no < prefix and no connection banner.

  3. 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/ws
    
    new: readyState 0
    error: Received network error or non-101 status code.
    after 3s: readyState 0

    Two things here are worth more than the error text. No close line appeared, and readyState is still 0 three seconds later, not 3.

  4. 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/socket
    
    new: readyState 0
    error: Received network error or non-101 status code.
    after 3s: readyState 0

    Byte 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.

  5. 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/socket
    
    error: Unexpected server response: 400

    Same 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

Sign: wscat -x prints nothing in CI and exits 0, so the pipeline passes.Cause: wscat builds a readline interface over stdin. With stdin closed, as it is under a scheduler or when redirected from a file, readline emits close, and wscat's close handler calls process.exit(0) before the reply arrives. Timed here at 0.158 s against a server that answers in milliseconds. Keeping stdin open, sleep 4 | wscat ..., returns the echo.
Sign: A reconnect loop written on onclose never retries after the first failure.Cause: Node's built-in WebSocket fires error but not close when the connection never opened, and readyState stays at 0 rather than moving to 3. Code that waits for close waits forever. Key the retry to error as well, and treat readyState 0 after your timeout as a failure.
Sign: The browser Network tab shows status 101 and the feature still does nothing.Cause: 101 records the handshake, not the session. Frames are on a separate panel, and an open socket with no frames is exactly what a handler bound to the wrong event or the wrong path produces.

What to check next

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.

basic6 minpublished updated Maks Verny