WebSocket load testing
A WebSocket connection is a long-lived, two-way channel. Load-testing it differs from HTTP load testing. Instead of short request-response cycles, each virtual user holds an open connection and exchanges messages over time. MaxoPerf runs WebSocket load tests through k6’s built-in WebSocket module.
Before you start
Section titled “Before you start”- Read k6 scripts on Maxoperf. WebSocket load testing uses k6 as the script engine.
- Learn the connection lifecycle of the service you test. Check whether it expects a handshake message after connect, and whether it pushes events or waits for the client to poll with messages.
k6 WebSocket basics
Section titled “k6 WebSocket basics”k6’s k6/ws module wraps the WebSocket connection lifecycle. Each VU opens one WebSocket connection and drives it:
import ws from 'k6/ws';import { check, sleep } from 'k6';
export const options = { vus: 50, duration: '3m', thresholds: { ws_connecting: ['p(95)<500'], // connection handshake under 500ms ws_session_duration: ['p(95)<180000'], // session holds for ~3 min },};
export default function () { const url = 'wss://realtime.example.com/ws'; const token = __ENV.WS_TOKEN; // injected from MaxoPerf secrets
const res = ws.connect( `${url}?token=${token}`, {}, function (socket) { socket.on('open', () => { // Send a subscription message after connecting socket.send(JSON.stringify({ type: 'subscribe', channel: 'prices' })); });
socket.on('message', (data) => { const msg = JSON.parse(data);
check(msg, { 'message has type': (m) => m.type !== undefined, 'no error payload': (m) => m.type !== 'error', });
// Respond to server ping if (msg.type === 'ping') { socket.send(JSON.stringify({ type: 'pong' })); } });
socket.on('error', (err) => { check(null, { 'no websocket error': () => false }); });
// Hold the connection for 30 seconds then close socket.setTimeout(() => { socket.close(); }, 30000); } );
check(res, { 'websocket connected': (r) => r && r.status === 101 });
sleep(1);}Connection-count testing
Section titled “Connection-count testing”WebSocket servers often have a per-instance connection limit. Teams commonly run a WebSocket load test to verify that:
- The server accepts N concurrent connections without degradation.
- Message broadcast latency stays within SLO as connection count grows.
- The server handles graceful close and reconnect correctly.
For connection-count testing, raise vus gradually and keep the per-connection message rate low:
export const options = { stages: [ { duration: '1m', target: 100 }, // ramp to 100 connections { duration: '5m', target: 100 }, // hold — observe connection stability { duration: '2m', target: 500 }, // ramp to 500 connections { duration: '5m', target: 500 }, // hold — observe at scale { duration: '1m', target: 0 }, // ramp down ],};Message-throughput testing
Section titled “Message-throughput testing”For services where throughput matters (e.g. order execution, real-time sensor ingest), each VU can send messages in a loop:
function (socket) { socket.on('open', () => { // Send one message per second for 30 seconds let count = 0; const interval = socket.setInterval(() => { socket.send(JSON.stringify({ type: 'order', symbol: 'AAPL', qty: 1, side: 'buy', ts: Date.now(), })); count++; if (count >= 30) { socket.clearInterval(interval); socket.close(); } }, 1000); });}Reading WebSocket results in MaxoPerf
Section titled “Reading WebSocket results in MaxoPerf”k6 reports WebSocket-specific metrics that appear in the MaxoPerf run-detail view:
| k6 metric | What it measures |
|---|---|
ws_connecting | Time to establish the WebSocket handshake (ms) |
ws_session_duration | Duration of each VU’s WebSocket session (ms) |
ws_msgs_sent | Total messages sent across all VUs |
ws_msgs_received | Total messages received across all VUs |
When the script makes no HTTP requests, the standard latency chart on the Overview tab shows ws_connecting (time to establish the connection). To measure round-trip latency, compute it in the script and emit a custom k6 trend metric:
import { Trend } from 'k6/metrics';const roundTripLatency = new Trend('ws_roundtrip_latency');
// In message handler:const sent = Date.now();socket.send(JSON.stringify({ type: 'ping_echo', ts: sent }));
socket.on('message', (data) => { const msg = JSON.parse(data); if (msg.type === 'ping_echo') { roundTripLatency.add(Date.now() - msg.ts); }});Authentication over WebSocket
Section titled “Authentication over WebSocket”Most WebSocket services authenticate with one of these:
- Query parameter token: pass it in the URL, as in
wss://example.com/ws?token=${TOKEN} - First message after connect: send a JSON auth message in the
openevent handler - Cookie: k6 sends cookies set during an HTTP login step in the WebSocket upgrade request
Use MaxoPerf secrets for any credentials: __ENV.WS_TOKEN.
Do / don’t
Section titled “Do / don’t”Do:
- Hold connections open as long as real user sessions last. Short connect/disconnect cycles miss the steady-state connection pressure.
- Handle
errorevents in the socket handler and usecheck()to register them as failures. - Use
socket.setTimeout()to close connections gracefully after the desired hold duration.
Don’t:
- Ignore
ws_session_duration. A session duration below your expected hold time means the server is closing connections under load. - Test unauthenticated WebSocket endpoints in production. Always confirm you are authorized to generate the connection volume you plan.
- Mix WebSocket and HTTP requests in the same k6 default function unless the service uses both in one session (e.g. HTTP to get an auth token, then WebSocket for realtime data).
Where to go next
Section titled “Where to go next”- k6 scripts on Maxoperf: k6 upload workflow.
- Soak / endurance test: test WebSocket connection stability over hours.
- SOAP and XML load testing: next protocol page.
- Cookbook: failure criteria / pass-fail gates: fail runs automatically when WebSocket latency exceeds thresholds.