WebSocket and SSE load testing: connections, messages and latency
Live score screens, chat, notification feeds, stock prices, order tracking: every screen where the server pushes data to the user uses WebSocket or Server-Sent Events (SSE). Their load differs from classic request/response traffic: the user does not send one request and leave, but keeps a connection open for minutes, sometimes hours. What strains the server is less the requests per second than the number of connections open at once and how fast a message reaches them.
WebSocket or SSE?
WebSocket is two-way: over one connection both client and server send messages whenever they like. It is used where the client talks often too, such as chat, games and collaborative editing. SSE is one-way: the client opens an event stream (text/event-stream) with an ordinary HTTP GET and the server sends events down it. It is enough for notification, price and status feeds, and works naturally with HTTP infrastructure (proxies, authentication, compression). What you measure differs between the two as well.
What are we measuring?
- Concurrent connections. Every open connection holds memory, a file descriptor and often a goroutine or thread on the server. A system fine at 10,000 connections may hit a memory or descriptor limit at 50,000. Model users as virtual users (VUs) and raise the count over minutes.
- Connection time. The handshake for WebSocket, opening the stream for SSE. Authentication, session checks and subscription setup happen while connecting, and this time is often longer than the message latency.
- Message latency. For WebSocket, the time to send a message and get its answer, or for a broadcast to reach the client; for SSE, the time from opening the stream to the first event and to the awaited ones. p95 and p99, not the average (the p95 and p99 guide).
- Drops. Do connections close unexpectedly, and do waits run out before the message arrives?
Three cases that get missed
- The reconnect storm. A deploy or a load balancer change drops every connection at once, and every client tries to reconnect within a few seconds. A system that carries thousands of open connections may not be able to set up that many at the same time. A spike test that raises the VU count in a very short time recreates this (test types).
- Proxy and load balancer timeouts. Proxies in the path close an idle connection after a while (often 60 s). Give the test a part with no messages so a drop shows up within that time. With SSE, a proxy that buffers the response delays events or sends them in batches; the time to the first event shows it right away.
- Broadcast (fan-out) load. One message to a room or topic is copied to thousands of connections. A test with few senders and many listeners is an entirely different load from one with many senders and few listeners. Make the room and topic distribution resemble production.
With Spitfire
The WebSocket step. Every VU keeps one connection per URL, like a browser tab: the first step that needs that URL connects, later steps and iterations reuse the connection, and if the server closed it the next step reconnects. So the number of open connections equals the number of VUs. The step has four actions:
- Send and wait for the reply: sends the message and waits for the next message, or the first one containing the text you give. The duration runs from sending to the reply; the reply goes to checks and extraction.
- Send: sends a message without waiting for a reply (text, or a binary frame given as base64).
- Wait for a message: waits for the next message the server pushes. Messages that arrive between steps are buffered per connection, so a notification arriving between two steps is not lost.
- Close the connection: closes it; the next WebSocket step reconnects. Useful for testing reconnect behaviour.
The handshake time is measured separately as the ws_connecting metric. Headers and subprotocols can be added to the handshake; the connection carries the cookies received at login, and a Client certificate (mTLS) can be chosen for internal services. If the wait (10 s by default) runs out before a message arrives, or the connection closes while waiting, the step fails. If the messages change data on the target, mark it on the step; a run then asks for write confirmation before it starts.
The SSE step. Opens the stream (GET, Accept: text/event-stream), reads until the expected number of events (1 by default) arrived, and closes the stream. You can count only events of a given type or whose data contains a given text. The step's duration runs to the last awaited event; the time to the first event is the sse_time_to_first_event metric. The last event's data goes to checks, its type and id are read from the Sse-Event and Sse-Id headers. If the response is not 200 or not an event stream, or the events do not arrive before the wait (30 s by default) runs out, the step fails. The stream stays open for the step; to model long-lived streams, raise the event count and the wait.
Steps that join a chat room, send a message and wait for its broadcast to come back, and read 5 events from a price stream; thresholds on handshake, message and first-event times.
"steps": [
{ "id": "join", "name": "Odaya katıl", "protocol": "ws",
"ws": { "action": "request", "url": "wss://chat.example.com/ws?token={{token}}",
"message": "{\"type\":\"join\",\"room\":\"{{room}}\"}", "match": "joined" } },
{ "id": "say", "name": "Mesaj gönder ve yayını bekle", "protocol": "ws",
"ws": { "action": "request", "url": "wss://chat.example.com/ws?token={{token}}",
"message": "{\"type\":\"say\",\"text\":\"{{$uuid}}\"}",
"match": "\"type\":\"said\"" },
"thinkTime": { "min": "2s", "max": "5s" } },
{ "id": "feed", "name": "Fiyat akışından 5 olay", "protocol": "sse",
"sse": { "url": "https://api.example.com/prices/stream",
"event": "price", "count": 5, "wait": "20s" } }
],
"thresholds": [
{ "metric": "ws_connecting", "expr": "p(95)<300" },
{ "metric": "req_duration", "filter": { "step": "say" }, "expr": "p(95)<200" },
{ "metric": "sse_time_to_first_event", "expr": "p(95)<1000" }
]During the run each step's requests per second, p95, p99 and error rate are watched live; WebSocket and SSE steps can make up a user journey together with an HTTP login and API calls in the same test.
Spitfire installs on Docker or Kubernetes with one command; every testing feature and protocol is open in the free edition.