How to test websocket message size limit

Set a known maxPayload on the server, then send one frame at the limit and one frame a byte over it. The frame at 8192 bytes echoes back. The 8193-byte frame produces no error on the sender: the connection closes with code 1009 and an empty reason.

Why check this

Run this before releasing anything that lets a user put content on a socket: a chat message carrying a pasted document, an editor sending a whole document state, a client that batches telemetry and flushes it in one frame. Run it again after a proxy or a gateway moves in front of the server, because the proxy has a cap of its own and it is rarely the same number.

The failure it prevents is a server that dies on one frame. A ws server with no error listener on the connection ends the Node process when one client sends one frame over the cap, and every other connected client drops with it. The client that caused it sees a clean close and no error, so the incident report starts at the wrong end of the wire.

Prerequisites

Save this as echo-server.mjs. The cap is set to 8192 bytes so that the limit is known rather than discovered.

// node echo-server.mjs   listens on ws://127.0.0.1:49317
import { WebSocketServer } from 'ws';

const PORT = 49317;
const MAX_PAYLOAD = 8192;

const wss = new WebSocketServer({ port: PORT, maxPayload: MAX_PAYLOAD });
let open = 0;

wss.on('connection', (ws) => {
  open += 1;
  ws.on('message', (data, isBinary) => ws.send(data, { binary: isBinary }));
  ws.on('error', (err) => console.log(`server socket error: ${err.message}`));
  ws.on('close', (code) => {
    open -= 1;
    console.log(`closed ${code}, open ${open}`);
  });
});

wss.on('listening', () =>
  console.log(`echo listening on ${PORT}, maxPayload ${MAX_PAYLOAD} bytes`));

Save this as size-probe.mjs. It reports every event the sender can observe, including the one that does not fire.

// node size-probe.mjs <port> <bytes>
import WebSocket from 'ws';

const [port, bytes] = process.argv.slice(2);
const ws = new WebSocket(`ws://127.0.0.1:${port}`);

ws.on('open', () => {
  console.log(`open, sending ${bytes} bytes`);
  ws.send('x'.repeat(Number(bytes)), (err) => console.log(`send callback error: ${err ?? 'none'}`));
});
ws.on('message', (d) => {
  console.log(`echo back: ${d.length} bytes`);
  ws.close(1000);
});
ws.on('error', (e) => console.log(`error event: ${e.message}`));
ws.on('close', (code, reason) => console.log(`close: code ${code}, reason "${reason}"`));

Step 6 needs the same server without its error handler. Save this as no-handler-server.mjs.

// node no-handler-server.mjs   listens on ws://127.0.0.1:49318
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 49318, maxPayload: 8192 });
wss.on('connection', (ws) => {
  ws.on('message', (data) => ws.send(data));
});
wss.on('listening', () => console.log('no-handler echo on 49318, maxPayload 8192 bytes'));

Steps

  1. Step 1.

    Start the server and keep its console visible. Its output is half the evidence.

    node echo-server.mjs > server.log 2>&1 &
    
    echo listening on 49317, maxPayload 8192 bytes
  2. Step 2.

    Read the default cap first, so you know what an unset maxPayload means on the server you are testing.

    // node default-maxpayload.mjs
    import { WebSocketServer } from 'ws';
    const wss = new WebSocketServer({ port: 49321 });
    console.log(`default maxPayload: ${wss.options.maxPayload} bytes`);
    wss.close();
    
    default maxPayload: 104857600 bytes

    100 MiB. A server that sets nothing accepts a frame far larger than anything the product needs.

  3. Step 3.

    Send a frame of exactly the cap. This is the case that has to keep working.

    node size-probe.mjs 49317 8192
    
    open, sending 8192 bytes
    send callback error: none
    echo back: 8192 bytes
    close: code 1000, reason ""

    maxPayload is inclusive. The frame at the stated limit comes back.

  4. Step 4.

    Send one byte more. Change nothing else.

    node size-probe.mjs 49317 8193
    
    open, sending 8193 bytes
    send callback error: none
    close: code 1009, reason ""

    Three lines worth reading. The send callback reports no error, because the frame did leave the sender. The error event never fires. The only signal is the close code, 1009, and the reason is an empty string.

  5. Step 5.

    Read the server's account of the same two connections.

    tail -4 server.log
    
    closed 1000, open 0
    closed 1000, open 0
    server socket error: Max payload size exceeded
    closed 1006, open 0

    The server logs 1006 for the connection it closed with 1009. It sent the peer 1009 and tore the socket down without waiting for the close handshake to come back, which is what 1006 records. One event, two numbers.

  6. Step 6.

    Delete the ws.on('error', ...) line from a copy of the server, start it on port 49318, and send 8193 bytes again.

    node no-handler-server.mjs > nohandler.log 2>&1 & sleep 2 && node size-probe.mjs 49318 8193 && cat nohandler.log
    
    open, sending 8193 bytes
    send callback error: none
    close: code 1009, reason ""
    no-handler echo on 49318, maxPayload 8192 bytes
    node:events:497
        throw er; // Unhandled 'error' event
        ^
    
    RangeError: Max payload size exceeded
      …
    code: 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH',
    [Symbol(status-code)]: 1009
    }
    
    Node.js v22.23.2

    The client half of that output matches step 4. The server is gone: netstat -ano | grep 49318 finds no listener. One frame from one client ended the process.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | echo back with the same byte count | The frame was inside the cap | Record the size that passed. It is the largest verified message. | | close: code 1009 and no error event | The receiver rejected the frame for its length | Handle close with code 1009 in the client, not error. | | reason "" | The receiver sent no explanation | Do not build user-facing text from the reason. Map 1009 to your own message. | | The server log says 1006 for that connection | The server closed without a completed handshake | Read both sides before putting a close code in a bug report. | | The server process exits | No error listener on the connection | Add one. Until then, one oversized frame is a denial of service. |

Common mistakes

Sign: The client has an error handler, it never fires, and the team concludes the message was delivered.Cause: A frame over maxPayload is rejected by the receiver after it left the sender. On the sender the send callback reports no error, no error event is emitted, and the entire signal is a close with code 1009 and an empty reason. A client that only handles error learns nothing.
Sign: The Node server exits with an unhandled RangeError and takes every other connection with it.Cause: ws emits the length error on the WebSocket instance. An EventEmitter with no error listener throws, so one client's oversized frame ends the process. A try/catch around ws.send does not cover it, because the throw happens on the receiving side, in a later tick.
Sign: The limit is tested at a round number, 1 MB or 64 KB, and it passes.Cause: maxPayload is inclusive, so a test at exactly the cap proves only that the accepted side works. The boundary needs both values: the cap, which must echo, and the cap plus one byte, which must close with 1009.
Sign: The server cap is raised and large messages still fail in one direction.Cause: maxPayload is a receiver setting and each side has its own. Setting it on the server changes what the server accepts, not what the client accepts from the server. Both ends default to 104857600 bytes and both have to be set.

Thresholds

104857600 bytes, 100 MiB, is the ws default maxPayload on the server and on the client

Read back from a running server by step 2 on ws 8.21.3. A service that sets nothing buffers frames three orders of magnitude larger than a chat message before the handler sees them.

Source: https://github.com/websockets/ws/blob/master/doc/ws.md

What to check next

FAQ

What close code means the message was too large?

1009, defined as Message Too Big. The receiver sends it and closes. In the run above the sender saw code 1009 with an empty reason and no error event, which is the whole notification it gets.

What is the default message size limit in ws?

104857600 bytes, 100 MiB, on the client and on the server. Step 2 reads it back from a running server. Set maxPayload to the largest message your product sends, plus headroom.

Why did the server log a different close code from the client?

The server sent 1009 to the peer and destroyed the socket without waiting for the close frame to come back. Its own close event then reports 1006, abnormal closure. Both numbers describe one event.

Does a try/catch around send catch this?

No. The frame is accepted locally and rejected by the receiver later. On the sending side nothing throws. On the receiving side the error arrives on the WebSocket instance, in a later tick, where only an error listener can take it.

How large should maxPayload be?

Measure the largest legitimate message in your traffic, set the cap above it, then test at the cap and one byte over. The default lets the server buffer 100 MiB per frame before your handler runs.

Verified

Verified by Maks VernyNode 22.23.2ws 8.21.3

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.

intermediate8 minpublished updated Maks Verny