How to check websocket message order
WebSocket delivers frames over one connection in the order they were sent, and 500 numbered frames came back with zero inversions. Reordering that you observe is your own code: an async handler returned five messages reversed, while the server log shows it received them in order.
Why check this
Run this the first time a bug report says messages arrive out of sequence, and again whenever a client starts opening a second socket or a handler gains an await. Both changes break an assumption the team never wrote down.
The failure it prevents is a week spent on the wrong layer. A chat that shows replies above questions, a board that applies a move before the move that set it up, a feed that jumps backwards: all three get filed against the transport, and the transport is the one part that is guaranteed here. RFC 6455 requires message fragments to be delivered in the order the sender sent them, and a WebSocket runs over a single TCP connection. The check below separates that guarantee from the code around it, so the search starts where the defect is.
Prerequisites
- Node 22 and
ws8 in an empty directory:npm i ws@8. - Two free ports.
netstat -ano | grep 49317prints nothing when 49317 is free, and the same for 49319. - RFC 6455 section 5.4 for the ordering rule, which reads "Message fragments MUST be delivered to the recipient in the order sent by the sender".
- Everything here runs on loopback on 2026-09-12. Loopback cannot reorder, and neither can a TCP connection over a real network: reordered packets are put back in sequence below the socket.
Save this as echo-server.mjs. It is the control: a handler with nothing asynchronous in it.
// 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 order.mjs. It numbers every frame and counts pairs that arrive smaller than their predecessor.
// node order.mjs <count>
import WebSocket from 'ws';
const N = Number(process.argv[2] ?? 5);
const ws = new WebSocket('ws://127.0.0.1:49317');
const got = [];
ws.on('open', () => {
for (let i = 1; i <= N; i += 1) ws.send(`m${i}`);
});
ws.on('message', (d) => {
got.push(d.toString());
if (got.length < N) return;
const seq = got.map((s) => Number(s.slice(1)));
const inversions = seq.filter((v, i) => i > 0 && v < seq[i - 1]).length;
if (N <= 10) console.log(`received: ${got.join(' ')}`);
console.log(`frames ${seq.length}, first ${got[0]}, last ${got[got.length - 1]}, out-of-order pairs ${inversions}`);
ws.close(1000);
});
Save this as async-server.mjs. Its handler does what a real handler does, which is wait for something.
// node async-server.mjs listens on ws://127.0.0.1:49319
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 49319 });
wss.on('connection', (ws) => {
ws.on('message', async (data) => {
const text = data.toString();
console.log(`received ${text}`);
const n = Number(text.slice(1));
await new Promise((r) => setTimeout(r, (6 - n) * 20)); // a lookup that varies per message
ws.send(text);
});
ws.on('error', (e) => console.log(`server socket error: ${e.message}`));
});
wss.on('listening', () => console.log('async handler echo on 49319'));
Steps
- Step 1.
Start the control server.
node echo-server.mjs > server.log 2>&1 &echo listening on 49317, maxPayload 8192 bytes - Step 2.
Send five numbered frames back to back on one connection.
node order.mjs 5received: m1 m2 m3 m4 m5 frames 5, first m1, last m5, out-of-order pairs 0 - Step 3.
Repeat at a size where a coincidence is no longer credible.
node order.mjs 500frames 500, first m1, last m500, out-of-order pairs 0Five hundred frames, no inverted pair. This is the baseline the rest of the check is measured against: the transport did not reorder anything.
- Step 4.
Start the server whose handler awaits, and send the same five frames to it.
// node order-async.mjs import WebSocket from 'ws'; const ws = new WebSocket('ws://127.0.0.1:49319'); const got = []; ws.on('open', () => { for (let i = 1; i <= 5; i += 1) ws.send(`m${i}`); }); ws.on('message', (d) => { got.push(d.toString()); if (got.length < 5) return; console.log(`client received: ${got.join(' ')}`); ws.close(1000); });node async-server.mjs > async.log 2>&1 & sleep 3; node order-async.mjs && sleep 1 && cat async.logclient received: m5 m4 m3 m2 m1 async handler echo on 49319 received m1 received m2 received m3 received m4 received m5Read the two halves against each other. The server received m1 through m5 in order, exactly as in step 2. The client got them back reversed. Nothing on the wire changed: the handler started five overlapping pieces of work and each one replied when it finished.
- Step 5.
Send one message on each of two connections and watch what the ordering rule covers.
// node two-conn.mjs import WebSocket from 'ws'; const a = new WebSocket('ws://127.0.0.1:49319'); const b = new WebSocket('ws://127.0.0.1:49319'); const order = []; let ready = 0; function go() { ready += 1; if (ready < 2) return; a.send('m1'); // sent first, handled slowly b.send('m5'); // sent second, handled quickly } a.on('open', go); b.on('open', go); function note(label) { return (d) => { order.push(`${label}:${d.toString()}`); if (order.length < 2) return; console.log(`sent order: A:m1 B:m5`); console.log(`received order: ${order.join(' ')}`); a.close(1000); b.close(1000); }; } a.on('message', note('A')); b.on('message', note('B'));sent order: A:m1 B:m5 received order: B:m5 A:m1The guarantee is per connection. Two sockets are two orderings, and the application has no sequence across them unless it puts one in the messages.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| out-of-order pairs 0 over 500 frames | The transport preserved order | Stop looking at the socket. The defect is above it. |
| Server log in order, client output reversed | The handler is asynchronous and unserialised | Await the previous reply, or carry a sequence number and sort on the client. |
| Out-of-order pairs on one connection with a synchronous handler | Something between you is not a plain WebSocket | Look for a proxy, a message broker or a fan-out layer that merges publishers. |
| Reordering that disappears when the client opens one socket | The second connection was the cause | Reuse one connection, or make ordering explicit in the payload. |
| Order correct under a slow run, wrong under load | Concurrency in the handler, exposed by timing | Reproduce with a deliberate delay, as in step 4, rather than with load. |
Common mistakes
What to check next
- How to test websocket latency: the handler cost that turns into reordering here is visible in the latency distribution.
- Websocket close code 1006: what ends a connection, and with it the ordering guarantee.
- How to test websocket reconnect: the gap where a message can arrive after one produced later.
- Socket io connection test: a layer above WebSocket that adds its own buffering and acknowledgements.
FAQ
Does WebSocket guarantee message order?
Within one connection, yes. RFC 6455 section 5.4 requires fragments to be delivered in the order sent, and the frames travel over one TCP stream. In the run above 500 numbered frames returned with zero inverted pairs.
Why do my messages arrive out of order then?
Most often an asynchronous handler. Step 4 reproduces it: the server received m1 through m5 in order and the client received m5 through m1, because each reply was sent when its own work finished.
Does the guarantee hold across two connections?
No. Two sockets are two independent orderings. In step 5 the message sent first on connection A arrived after the message sent second on connection B. Carry a sequence number if you need order across sockets.
How many messages should I test with?
Enough that an accidental pass is unlikely. Five frames prove little, 500 with zero inversions is a baseline, and the interesting run is the one with a varying delay in the handler.
Can a proxy reorder WebSocket frames?
A conforming proxy relays the stream and cannot reorder it. A message broker or a fan-out service in the middle can, because it is re-emitting messages rather than forwarding bytes. Test through the same path production uses.
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
intermediate9 minpublished updated Maks Verny