WebSocket Testing
A WebSocket is a long-lived, two-way connection between a client and a server, defined in RFC 6455. It starts as an HTTP request that asks to upgrade the connection; after that, either side can send a message at any moment, with no request-and-response pairs. Chat, live tracking and trading screens use it so that the server can push changes immediately.
Example: a delivery app opens one connection on the order page and subscribes to order A-1042. When the order changes, the server pushes {"type":"status","seq":2,"status":"picked_up"} and the page updates without reloading. Testing it means checking how the connection opens, what arrives and in what order, how it closes, and what happens when it breaks or when many clients connect.
Why it matters
There is no status code per message, so a lost, duplicated or out-of-order update raises no error: the customer just sees "picked up" after "delivered", or a chat message twice. And because connections stay open for hours, some failures never appear in a quick check: a proxy that cuts idle connections, a session that expires mid-connection, server memory that grows with every client.
The tester's question changes from "what does this request return?" to "what must arrive, in what order, within what time, and what happens after a drop?"
The opening handshake
The client sends an HTTP/1.1 GET with Upgrade: websocket, Sec-WebSocket-Version: 13 and a random Sec-WebSocket-Key. A server that accepts answers 101 Switching Protocols with a Sec-WebSocket-Accept value derived from that key; from then on the same TCP connection carries WebSocket frames. Production should use wss:// (over TLS), not ws://.
In this HTTP/1.1 handshake any other status means no WebSocket was opened (over HTTP/2, RFC 8441 uses a CONNECT request answered with 200 instead), so access rules are tested here. Cookies travel with the handshake, and RFC 6455 says a server should answer 403 when the Origin header names a site it does not accept. Browser scripts cannot add custom headers, so tokens often travel in the URL, where they can end up in logs. A script never sees the status of a rejected handshake, only a failed connection with close code 1006; read the real status in the developer tools' Network panel.
- No session, an expired session or no access: no
101, but the401or403the contract names. - An origin the server does not accept:
403. A browser always sends the origin of the page it runs on, so connecting from another site, such as the WebSocket Tester, already tests a foreign origin; a missing or forgedOriginneeds a client outside the browser. Test only systems you own or have written permission to test. - A page served over HTTPS that opens
ws://: the browser blocks it as mixed content.
GET /live HTTP/1.1
Host: shop.example.test
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://shop.example.test
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Messages, control frames and close codes
Data travels in frames. A message is text (UTF-8) or binary and may span several frames. A ping control frame must be answered with a pong; browsers do this automatically but hide both from scripts, so applications often add their own heartbeat message. There are no built-in acknowledgements: if the client must know that a message was processed, the application defines a reply, usually echoing an id.
Test messages as you would a REST body: required fields, types, unknown message types, malformed JSON, and the size limit and one byte above it. Either side ends the connection with a close frame, which normally carries a code and an optional reason; the other side answers with its own close frame. The RFC 6455 codes you meet most:
1000normal closure;1001going away, such as a server shutting down or a browser leaving the page.1003a data type the endpoint cannot accept;1007data that does not fit the message type, such as invalid UTF-8 in text;1008policy violation;1009message too big;1011an unexpected server condition, the counterpart of a500.1005and1006are never sent in a close frame: an endpoint reports1005itself when the close frame it received had no code, and1006when the connection ended without a close frame, after a network drop, a killed process or a proxy timeout.- Codes from 4000 to 4999 are for private use: an API may define
4401for an expired session, and it means only what that API's documentation says.
- Client Order page in the browser
- Server
wss://shop.example.test/live
- 1 From Client To Server: HTTP GET asking to upgrade With
Sec-WebSocket-Keyand version 13; the session cookie goes alongUpgrade: websocket - 2 From Server To Client: Handshake accepted reply
Sec-WebSocket-Acceptis derived from the key; any other status means no WebSocket101 Switching Protocols - 3 From Client To Server: Subscribes to order A-1042
{"type":"subscribe","lastSeq":0} - 4 From Server To Client: Confirms the subscription reply
{"type":"subscribed"} - 5 From Server To Client: Pushes an update nobody asked for
{"seq":1,"status":"accepted"} - 6 From Server To Client: Heartbeat while nothing changes Every 20 s, so that no proxy cuts a silent connection
{"type":"heartbeat"} - 7 From Server To Client: Next update, next sequence number
{"seq":2,"status":"picked_up"} - 8 From Client To Server: User closes tracking: close frame Normal closure sent by the app; leaving the page would make the browser send 1001
1000 - 9 From Server To Client: Answers with its own close frame, then TCP closes reply
1000
Outcome: Clean close: both sides sent a close frame. Had the connection just dropped, the client would report 1006.
Testing ordering and reconnection
On one connection, frames arrive in the order they were sent; WebSocket runs over TCP and nothing reorders them. Ordering defects come from the application: several server workers publishing, a replay after a reconnect, asynchronous handling in the client. A sequence number in every message gives the test something to check.
Reconnection is the application's job. RFC 6455 recommends a random delay before the first attempt and longer delays after each failure (back-off), so that thousands of clients do not hit a recovering server at once. After reconnecting, the client must authenticate again, resubscribe and catch up on what it missed.
- Client Order page in the browser
- Server
wss://shop.example.test/live
- 1 From Server To Client: Update 1 arrives
{"seq":1,"status":"accepted"} - 2 Client internally: The network drops: no close frame The client reports 1006; updates 2 and 3 happen meanwhile
1006 - 3 Client internally: Waits a random delay, longer after each failure Back-off, as RFC 6455 recommends
- 4 From Client To Server: New handshake Authenticates again with the session cookie
Upgrade: websocket - 5 From Server To Client: Accepted reply
101 Switching Protocols - 6 From Client To Server: Resubscribes with the highest
seqit received{"type":"subscribe","lastSeq":1} - 7 From Server To Client: Replays only what was missed reply
{"seq":2,"status":"picked_up"} - 8 From Server To Client: Then the next one, in order
{"seq":3,"status":"delivered"} - 9 Client internally: Checks
seq: no gap, no duplicate Pass
Outcome: A repeated update 1, or a missing update 2, would be a defect even though the page ends on the right status.
- Ordering: send a burst of numbered messages, and events from two clients at once; every receiver sees the same order, without gaps or duplicates.
- Drop: cut the network mid-session (switch off Wi-Fi, or stop the proxy), let events happen, then restore it; every missed update appears exactly once.
- Idle: stay silent longer than the proxy timeout; the heartbeat keeps the connection open, or the client recovers.
- Session expired or access revoked while connected: the server stops sending private data, and the client asks the user to sign in instead of reconnecting forever.
WebSockets under load
Requests per second matter less here than concurrent open connections, the rate of new handshakes, messages delivered per second and the delay from an event to its arrival at every subscriber. Every open connection holds memory and a socket on the server and on each proxy, even when silent. Tools such as k6 and Gatling can script WebSocket clients; load only environments you are allowed to load.
- Ramp up to the expected peak of connections and hold it, giving each simulated client its own test user and token as real users would have; record failed handshakes and memory per connection.
- Fan-out: one event to thousands of subscribers; report delivery latency at the median and the 95th percentile (the time that 95% of deliveries stay within), measured on one machine's clock or with synchronised clocks.
- Reconnection storm: drop all connections at once, as a deployment does; back-off should spread the reconnects and the service should recover.
- Slow consumers and soak (a long run at normal load): a client that reads slowly must not make the server queue messages without limit, and memory must not grow over hours.
Worked example: a live order-tracking feed
The delivery app's contract: seq rises by one per update and a status never moves backwards; a heartbeat arrives every 20 seconds when nothing changes; after a reconnect the client sends its lastSeq and the server replays only updates with a higher seq; close codes are 1000 (user closed tracking), 1008 (invalid message) and 4401 (session expired, do not reconnect). In the sample, -> is client to server and <- is server to client.
Test conditions derived from it, each observable in the frame log:
- Subscribing to another customer's order
B-2077delivers none of its updates. The contract does not say whether the server answers with an error message or a close code: a gap in the test basis to settle with the team first. - A subscribe without
orderId, or text that is not JSON, closes with1008; a1011means the server failed on bad input. - With no change for a minute, a heartbeat arrives about every 20 seconds and the connection stays open.
- Cut the network after
seq1, advance the order twice, restore it: the client resubscribes withlastSeq1 and receives 2 and 3 once, in order. - When the session expires, the client receives
4401, stops reconnecting and asks the user to sign in.
-> {"type":"subscribe","orderId":"A-1042","lastSeq":0}
<- {"type":"subscribed","orderId":"A-1042"}
<- {"type":"status","orderId":"A-1042","seq":1,"status":"accepted"}
<- {"type":"heartbeat"}
<- {"type":"status","orderId":"A-1042","seq":2,"status":"picked_up"}
<- {"type":"status","orderId":"A-1042","seq":3,"status":"delivered"}Common mistakes
- Testing only the happy path in one tab; most WebSocket defects appear after a drop, an idle period or under many connections.
- Treating a sent message as delivered; without an acknowledgement, a message sent just before a drop can be lost.
- Checking the final screen instead of the sequence numbers, which reveal gaps, duplicates and reordering.
- Testing authentication only at the handshake, never when the session expires while the connection is open.
- Using fixed waits in automated tests; wait for the expected message with a timeout, or the tests become slow and flaky.
The QA perspective
Real-time features often arrive as one sentence: "the page updates live". In refinement, ask for the message contract: message types and fields, sequence numbers, the heartbeat interval, close codes and the client's reaction to each, the reconnect policy and how missed messages are recovered. Some teams write it as an AsyncAPI document; without it, ordering and reconnection have no expected result.
Keep a timestamped frame log while testing; the WebSocket Tester can copy or download one. A bug report gives the URL, the steps, the frames around the failure with their sequence numbers, the close code and whether the close was clean. Agree load targets with developers and operations before the first run.
Practice exercise
Find the defects in a chat session log
A support chat's contract: seq rises by 1 per message in the room; a quiet room gets {"type":"heartbeat"} every 20 seconds; after a drop the client rejoins with lastSeq and the server replays only messages with a higher seq; close codes are 1000 (user left the room), 1008 (invalid message) and 4403 (no access to the room).
Compare the captured log below with the contract. List what the log proves, what it only suggests and what worked, with one follow-up test per finding.
10:00:00.120 info Connected wss://chat.example.test/ws
10:00:00.180 sent {"type":"join","room":"support-17","lastSeq":0}
10:00:00.250 recv {"type":"joined","room":"support-17"}
10:00:04.010 recv {"type":"msg","seq":1,"text":"Hello, how can I help?"}
10:00:09.400 recv {"type":"msg","seq":3,"text":"I am checking your order."}
10:00:09.460 recv {"type":"msg","seq":2,"text":"Please share the order number."}
10:01:09.470 info Closed: code 1006, clean: false
10:01:09.980 info Reconnecting (attempt 1)
10:01:10.150 info Connected wss://chat.example.test/ws
10:01:10.200 sent {"type":"join","room":"support-17","lastSeq":3}
10:01:10.270 recv {"type":"joined","room":"support-17"}
10:01:10.300 recv {"type":"msg","seq":3,"text":"I am checking your order."}- Note what each contract rule lets you check in a log.
- Walk the log: sequence numbers, time gaps, the close code, the rejoin and the replay.
- Separate proof from suspicion that needs server or proxy logs.
- Write one follow-up test per finding.
Show the model answer
Proven: seq 3 arrived before seq 2 on one connection, so the customer reads "I am checking your order" before being asked for the order number. Frames are not reordered in transit, so the server sent them that way. Follow-up: 50 quick messages from the agent, then check the received seq list.
Proven: 60 seconds of silence (10:00:09.460 to 10:01:09.470) without the required heartbeat. Suggested: 1006 means no close frame, and a cut after exactly 60 silent seconds points to a proxy idle timeout; confirm it in the proxy logs. Follow-up: leave a quiet room open for 3 minutes.
Correct: the client started reconnecting about half a second after the drop and rejoined with lastSeq 3, the highest seq received, not 2, the last one to arrive.
Proven: the server replayed seq 3 although lastSeq was 3, so the client received it twice and, unless it drops duplicates, shows it twice. Likely cause: an off-by-one, seq >= lastSeq instead of seq > lastSeq. Follow-up: rejoin with lastSeq 1 and expect exactly 2 and 3.
Not covered: 1000, 1008 and 4403; add tests for leaving, an invalid message and a forbidden room.
Frequently asked questions
How is WebSocket testing different from REST API testing?
A REST test sends one request and checks one response. A WebSocket test checks a stream on an open connection: which messages arrive, in what order and how fast, what happens to the connection over time and how it ends. REST skills still apply, such as checking payloads against a contract, authentication and negative input, but there is no status code per message.
Are the browser developer tools enough?
For observing, yes: the Network panel of the major browsers lists a page's WebSocket connection and its messages. To send your own messages, reconnect or keep a log, use a client such as the WebSocket Tester. A browser client always sends its page's origin and cannot set custom handshake headers, so a missing or forged Origin, or a token in a header, needs a client outside the browser.
What does close code 1006 mean?
The connection ended without a close frame, so no code was received. No endpoint ever sends 1006; the WebSocket library on the side that saw the connection end reports it. Common causes are a network drop, a crashed or restarted server process, or a proxy closing idle connections; in a browser, a handshake the server rejected also ends as 1006. Check the timing, the Network panel and the server and proxy logs.
How many connections should a load test open?
Base it on data: the peak of users online at the same time from production analytics or the business forecast, times the connections per user (often one per tab or device), plus a margin the team agrees. Then measure delivery latency and errors at that level, and find the level where the target stops being met.
References
- RFC 6455: The WebSocket ProtocolThe WebSocket handshake over HTTP, framing, masking, ping/pong and close codes.
- The WebSocket API — MDNThe browser WebSocket API: opening a connection, sending messages and handling close events.
- OWASP Web Security Testing Guide (WSTG)The reference methodology for testing web applications and APIs for security weaknesses, test by test.
Practise with Mutqan tools
Connect to WebSocket servers, send messages and watch incoming frames in real time — directly from your browser.
Validate a JSON document against a JSON Schema (draft-07 / 2019-09 / 2020-12 core keywords).
Format, beautify and validate JSON instantly in your browser with custom indentation.
Generate large parameter datasets (users, tokens, ids, payloads) for k6, JMeter, Gatling and Locust load tests.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.