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
| Setting | Value | Why |
|---|---|---|
| Concurrent connections | 5,000, ramped over 5 minutes | Enough to hit the per-process limits (file descriptors, event-loop or thread pools) most servers ship with. |
| Duration | 15 minutes | Connections must stay open long enough for heartbeats, idle timeouts and memory growth to show. |
| Message rate | 1 message every 5 s per connection | About 1,000 messages/s at full load; raise it to model busier rooms. |
| Message size | 200 to 500 bytes | A chat line or a state update. Large payloads test bandwidth, not concurrency. |
| Regions | 2 or more | WebSocket connections pass through load balancers and proxies with their own idle limits; a second region exposes them. |
| Protocol | wss:// with the same auth the app uses | Handshake 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
| Threshold | Target | What a breach means |
|---|---|---|
| Connect time p95 | < 500 ms | The handshake is queueing; check accept backlog and TLS termination. |
| Message latency p95 | < 200 ms | Fan-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→