Skip to content

Request/Response over WebSocket

Developers coming from HTTP carry a deep assumption: I called, so this response is mine. With fetch, the answer you await is unambiguously the answer to the question you asked. A WebSocket quietly removes that guarantee — and getting it back is one of the most useful patterns in message design.

The socket is message-oriented, not request/reply

Section titled “The socket is message-oriented, not request/reply”

A WebSocket has no concept of a request paired with a response. There is only a stream of messages flowing each way, independently. When onmessage fires, the message could be:

  • a reply to something you just asked,
  • a reply to something you asked three messages ago that took longer,
  • or a completely unsolicited server push (a new chat line, a price tick).

If you fire off three “fetch user” requests and three results come back, nothing in the transport tells you which result answers which request. Replies can arrive out of order, interleaved with pushes. You cannot rely on ordering, and you certainly cannot await a send.

Correlation ids tie a reply to its request

Section titled “Correlation ids tie a reply to its request”

The fix is a single field you already know how to add: a unique id on each request, which the server echoes on the matching reply. The client keeps a small table of id → “who is waiting for this”, and when a reply arrives it looks up the id, resolves that waiter, and removes the entry. Replies for unknown ids (or messages with no id at all) are treated as server pushes.

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: { type: 'getUser', id: 'r1', name: 'alice' }
  C->>S: { type: 'getUser', id: 'r2', name: 'bob' }
  Note over S: replies may come back in any order
  S-->>C: { type: 'reply', id: 'r2', age: 41 }
  C->>C: look up r2, resolve bob's waiter
  S-->>C: { type: 'reply', id: 'r1', age: 30 }
  C->>C: look up r1, resolve alice's waiter
Correlating replies to requests with an echoed id

This is exactly how request/response feels like HTTP again — but built by hand, on top of a stream, with a single correlation field. Wrapping it in a promise makes the call site read like an ordinary await.

The demo below implements request() over the in-page echo socket. Each call generates a fresh id, stores a resolver in a pending map, and sends the request. When a reply comes back, its echoed id finds the right resolver. Two requests go out; the echo delays them differently so the replies arrive out of order — and correlation still matches each one correctly. It uses the real WebSocket API.

JavaScript

Watch the order: bob’s reply comes back first because the server answered him faster, yet alice’s promise still resolves to alice and bob’s to bob. The id — not arrival order — is what makes the match. That little pending map is the whole trick behind every “RPC over WebSocket” library you will ever use.

A couple of refinements you would add in production: a timeout that rejects a pending entry if no reply arrives, and cleanup of the pending map if the socket closes so no promise hangs forever.

Why can you not rely on arrival order to match WebSocket replies to requests?
How does the correlation-id pattern match a reply to its request?
In the demo, why does alice still resolve to alice even though bob replies first?