Concurrency Testing for WebSocket Connections in Live Applications

Hold thousands of concurrent WebSocket connections, send messages at a fixed rate and measure connect time, message latency and drops. k6 script included.


Hold thousands of WebSocket connections open at once

HTTP load tests measure request and response. A chat, trading or collaboration app fails differently: the server holds a socket per user, memory and file descriptors grow with every connection, and the first thing to break is usually the connection count, not the message rate. This template opens connections and keeps them, sends a small message every few seconds like a real client, and measures connect time, message round trip and drops as the count climbs.

Configuration

SettingValueWhy
Concurrent connections5,000, ramped over 5 minutesEnough to hit the per-process limits (file descriptors, event-loop or thread pools) most servers ship with.
Duration15 minutesConnections must stay open long enough for heartbeats, idle timeouts and memory growth to show.
Message rate1 message every 5 s per connectionAbout 1,000 messages/s at full load; raise it to model busier rooms.
Message size200 to 500 bytesA chat line or a state update. Large payloads test bandwidth, not concurrency.
Regions2 or moreWebSocket connections pass through load balancers and proxies with their own idle limits; a second region exposes them.
Protocolwss:// with the same auth the app usesHandshake cost with TLS and a token is part of what you measure.

Run as a k6 cloud testOpens the k6 test form. Upload the script below, set virtual users and duration, and start; the free plan runs it at the free user limit.

WebSocket scenarios run as a k6 script (the cloud test form is for HTTP requests). Upload the script below to a k6 cloud test, pick the regions and the user count, and start it; k6 keeps every virtual user’s socket open for the whole iteration.

What to read in the results

  • Connect time as connections climb. The handshake should cost the same at 4,000 connections as at 40. When it climbs, the server is spending time in accept queues or TLS; when it fails, you have found the descriptor or connection limit.
  • Message round trip. Measured from send to the echo or acknowledgement. A flat line that jumps at one connection count is a saturated event loop or a broadcast fan-out that scales with the room size.
  • Drops and reconnects. Sockets closed by the server (or a proxy) under load show as errors; a burst of closes at exactly 60 s or 300 s is an idle timeout on a load balancer, not your code.

Pass/fail thresholds for this template

ThresholdTargetWhat a breach means
Connect time p95< 500 msThe handshake is queueing; check accept backlog and TLS termination.
Message latency p95< 200 msFan-out or the event loop is saturated at this connection count.
Error rate< 0.5%Connections are being refused or dropped; find the limit that tripped.

Set these on the k6 test in LoadFocus and every run gets a PASS or FAIL; the k6 thresholds in the script fail the run locally as well.

The same scenario as a k6 script

The script opens one socket per virtual user, keeps it for the iteration, and sends a heartbeat every 5 seconds. Replace the URL and the auth token with yours.

import ws from 'k6/ws';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '5m', target: 5000 },
    { duration: '10m', target: 5000 },
  ],
  thresholds: {
    ws_connecting: ['p(95)<500'],
    ws_session_duration: ['p(95)>280000'],
  },
};

export default function () {
  const url = 'wss://chat.example.com/socket?token=' + __ENV.LF_TOKEN;
  const res = ws.connect(url, {}, function (socket) {
    socket.on('open', () => socket.setInterval(() => socket.send(JSON.stringify({ type: 'ping', t: Date.now() })), 5000));
    socket.on('message', (msg) => { const m = JSON.parse(msg); if (m.type === 'pong') check(m, { 'pong under 200ms': () => Date.now() - m.t < 200 }); });
    socket.setTimeout(() => socket.close(), 300000); // hold the socket 5 minutes, then reconnect
  });
  check(res, { 'handshake 101': (r) => r && r.status === 101 });
}

When to run it

  • Before a launch that will add users to rooms or channels that already exist.
  • After changing the load balancer, the proxy or the WebSocket library.
  • Whenever a heartbeat interval changes idle timeouts and heartbeats interact, and the failure is silent until it is not.

FAQ on WebSocket load testing

Can the no-code cloud test do WebSockets?

No. The cloud test form sends HTTP requests. WebSocket scenarios run as k6 scripts on LoadFocus, which supports the ws:// and wss:// protocols through the k6/ws module.

How many connections can one load generator open?

Thousands per generator; the limit is usually the target. LoadFocus spreads virtual users across cloud agents, so 5,000 connections from three regions is a normal run.

The test stops at exactly 1,024 or 4,096 connections. Why?

That is a file-descriptor limit on the server or a proxy in front of it (ulimit, or a per-worker cap in nginx or HAProxy). Raise it and run again.

What about Socket.IO or SignalR?

Both negotiate over HTTP before upgrading. Model the negotiation as an HTTP request in the script, then connect to the returned endpoint with k6/ws.

How fast is your website?

Elevate its speed and SEO seamlessly with our Free Speed Test.

Outgrown your testing tools?

Load test websites and APIs from 25+ cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.Start for free
jmeter cloud load testing tool

Free Website Speed Test

Analyze your website's load speed and improve its performance with our free page speed checker.

×