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
- Node 22 and
ws8 in an empty directory:npm i ws@8. Node 22 ships a globalWebSocketclient and no server, sowssupplies both halves. The ws API documentation names every option used here. - A free port.
netstat -ano | grep 49317prints nothing when 49317 is free. - Everything below runs on loopback, on one Windows machine, on 2026-09-12. The fact under test is the receiver's frame cap, not the network.
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
- 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 - Step 2.
Read the default cap first, so you know what an unset
maxPayloadmeans 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 bytes100 MiB. A server that sets nothing accepts a frame far larger than anything the product needs.
- Step 3.
Send a frame of exactly the cap. This is the case that has to keep working.
node size-probe.mjs 49317 8192open, sending 8192 bytes send callback error: none echo back: 8192 bytes close: code 1000, reason ""maxPayloadis inclusive. The frame at the stated limit comes back. - Step 4.
Send one byte more. Change nothing else.
node size-probe.mjs 49317 8193open, 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
errorevent never fires. The only signal is the close code, 1009, and the reason is an empty string. - Step 5.
Read the server's account of the same two connections.
tail -4 server.logclosed 1000, open 0 closed 1000, open 0 server socket error: Max payload size exceeded closed 1006, open 0The 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.
- 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.logopen, 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.2The client half of that output matches step 4. The server is gone:
netstat -ano | grep 49318finds 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
Thresholds
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.mdWhat to check next
- Websocket close code 1006: 1009 and 1006 appeared on the two sides of the same close here.
- Websocket binary frame: the cap counts bytes, so a binary frame reaches it sooner than the text it encodes.
- Websocket compression: permessage-deflate changes what a frame weighs on the wire, and the cap applies after decompression.
- How to test websocket connection: the handshake this procedure assumes has already succeeded.
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.
Related on this site
intermediate8 minpublished updated Maks Verny