By Performate
SSE and Long-Polling Under Load: A Practical k6 Testing Pattern
Load-test SSE and long-polling: concurrent streams, proxy idle timeouts, and k6 patterns—MDN, HTTP/1.1 behavior, and comparison with WebSockets.
Dashboards that SSE or long-poll stall under concurrent streams fail differently than REST bursts—proxies idle-timeout, connection caps, and buffer bloat dominate (MDN SSE, HTTP keep-alive). A spike of short GETs tells you nothing about ten thousand open streams behind the same load balancer.
k6 can open long-lived HTTP connections; model concurrent stream count and long request duration, not only RPS of sub-second GETs. This guide covers failure modes, a concurrent VU pattern with extended timeouts, and when to compare with WebSocket load patterns.
Failure modes under concurrent streams
Event-driven UIs and live feeds stress infrastructure REST benchmarks skip:
- Proxy closes idle connections at 60s while server believes stream is still open—clients reconnect storm amplifies load.
- File descriptor exhaustion on gateways and ingress controllers—errors look like random 502s.
- Fan-out: one server event triggers N client polls or downstream fetches—multiply load beyond k6 VU count.
- Buffer bloat on slow consumers—memory rises on intermediaries while k6 still receives 200.
- HTTP/1.1 connection reuse limits per host—VU count != browser tab count but directionally similar.
Long-polling differs slightly: repeated long GETs per VU cycle instead of one infinitely open stream—model both if product uses hybrid patterns.
SSE vs WebSocket vs long-poll
| Pattern | Load model | k6 note |
|---|---|---|
| SSE | Many concurrent open GETs | Accept: text/event-stream, long timeout |
| Long-poll | Repeated long GETs per VU | Shorter loop with timeout near server hold |
| WebSocket | Bidirectional frames | See WebSocket k6 guide; may need xk6 for advanced cases |
k6 pattern: long GET with extended timeout
Illustrative—not production-ready. k6 HTTP client reads full response body by default—for production-grade SSE parser validation consider companion tools or xk6 extensions; this pattern still stress-tests connection tables and proxy timeouts.
What this demonstrates:
constant-vusequals concurrent open streams—tuneSTREAM_VUSto expected live viewers, not REST RPS.- Extended
timeoutonhttp.get—120s example; align with server long-poll hold + margin. Accept: text/event-streamheader—routes through same path browsers use.- Tags
protocol:sseseparate thresholds from REST scenarios in mixed suites.
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE = __ENV.API_BASE || 'https://staging.example.com';
export const options = {
scenarios: {
sse_clients: {
executor: 'constant-vus',
vus: Number(__ENV.STREAM_VUS || 100),
duration: '5m',
tags: { protocol: 'sse' },
},
rest_baseline: {
executor: 'constant-arrival-rate',
rate: 20,
timeUnit: '1s',
duration: '5m',
tags: { protocol: 'rest' },
},
},
thresholds: {
http_req_failed: ['rate<0.02'],
'http_req_duration{protocol:sse}': ['p(95)<5000'],
'http_req_duration{protocol:rest}': ['p(95)<400'],
},
};
export default function () {
const res = http.get(`${BASE}/events/stream`, {
headers: { Accept: 'text/event-stream' },
timeout: '120s',
tags: { route: 'stream', protocol: 'sse' },
});
check(res, { connected: (r) => r.status === 200 });
sleep(Number(__ENV.RECONNECT_SEC || 5));
}
Patterns that work
- Ramp VUs in stages—find connection limit knee before full viewer count.
- Match proxy/load balancer idle timeouts in test plan doc—if proxy kills at 60s, do not use 120s timeout without expecting reconnect churn.
- Monitor open FDs on gateway during test—abort if slope linear.
- Separate REST and SSE scenarios—do not merge thresholds; failure modes differ.
Anti-patterns to avoid
- Measuring only short REST health while production pain is concurrent streams.
- Setting VUs to REST RPS numbers—streams are concurrent connections, not req/s.
- Ignoring reconnect storms after proxy timeout—second wave may exceed first.
- Running full viewer count on production without change control.
Pro tip (example command): ramp stream VUs to find knee.
k6 run sse-load.js --env STREAM_VUS=50 --duration 3m --tag protocol=sse
What this command demonstrates: incremental VU trials on desktop locate gateway FD limits before scheduling the full 5k stream rehearsal with platform on-call.
Decision table: pattern vs load model
| Pattern | Load model | Primary metric |
|---|---|---|
| SSE push | Many concurrent open GETs | Concurrent VUs + stream errors |
| Long-poll | Repeated long GETs per VU | Cycle time + timeout rate |
| REST + SSE mixed | Separate scenarios | Do not merge p95 |
| Fan-out backend | Add downstream mock or monitor | Broker/queue depth if applicable |
Run REST baseline scenario in parallel—proves API still healthy while streams open—some teams only break REST when connection table full.
Observability and pre-run checklist
- Match proxy/load balancer idle timeouts documented in runbook.
- Monitor open FDs and connection count on gateway during test.
- Abort if error rate climbs in first 10 minutes—do not burn staging for full duration blindly.
- Coordinate with networking team—shared ingress may affect unrelated services.
- Note k6 HTTP limitations for event parsing—document if test is connection-only vs payload validation.
- Compare results to WebSocket path if product considers migration.
How Performate supports SSE/long-poll testing
Below is a concrete workflow example for stream endpoint load—adapt VU counts to your peak concurrent viewers.
Example: stream VU ramp with export for networking
- Import stream endpoint from Postman—headers including
Accept. Problem solved: same path QA uses functionally becomes load target. - Set VU scenario + long timeout in UI—120s or per runbook. Problem solved: avoids hand-editing timeout strings across iterations.
- Run staged ramp on VUs—50, 100, 200 with abort tripwires. Problem solved: find knee before full-scale rehearsal.
- Export timeout and connection errors filtered by
protocol:sse. Problem solved: networking team gets status code breakdown, not generic "slow." - Share export with platform—FD chart screenshot same timestamp. Problem solved: capacity ticket links app test to infra evidence.
- Save template for release—retag
protocol:sseper event. Problem solved: repeat before each live dashboard launch.
That workflow maps directly to the cta in this post: runnable long-connection scenarios without glue-code weeks.
Closing takeaway
SSE/long-poll load is a connection budget problem—ramp concurrent streams, align timeouts with proxy rules, and never merge SSE thresholds with REST baselines.
Schedule a VU ramp this week on staging, watch gateway connection metrics during the run, and record the VU count where errors begin—that number belongs in the launch checklist.
Ready to optimize your API performance?
Use Performate to turn this playbook into runnable k6 scenarios, thresholds, and shareable reports without losing days to glue code.