WebSocket load testing
WebSocket connections are long-lived, bidirectional communication channels. Load-testing WebSocket services is different from HTTP load testing: instead of short request-response cycles, each virtual user holds an open connection and exchanges messages over time. MaxoPerf supports WebSocket load testing 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.
- Understand the connection lifecycle of the WebSocket service you are testing: does it expect a handshake message after connect? Does it push events, or does the client 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. A WebSocket load test is commonly used to verify:
- 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, increase vus gradually while keeping 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 |
The standard latency chart on the Overview tab reflects ws_connecting (connection establishment time) when no HTTP requests are made. For round-trip latency measurement, 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 via:
- Query parameter token — pass in the URL:
wss://example.com/ws?token=${TOKEN} - First message after connect — send a JSON auth message in the
openevent handler - Cookie — cookies set during an HTTP login step are sent in the WebSocket upgrade request automatically by k6
Use MaxoPerf secrets for any credentials: __ENV.WS_TOKEN.
Do / don’t
Section titled “Do / don’t”Do:
- Hold connections open long enough to reflect real user sessions — 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 that drops below your expected hold time indicates the server is closing connections under load. - Test unauthenticated WebSocket endpoints in production — always verify you have authorization to generate the connection volume you plan to run.
- Mix WebSocket and HTTP requests in the same k6 default function unless the service genuinely uses both in the same session (e.g. HTTP to get 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.