Skip to main content
Jun 29, 2026server-sent events load testing

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

PatternLoad modelk6 note
SSEMany concurrent open GETsAccept: text/event-stream, long timeout
Long-pollRepeated long GETs per VUShorter loop with timeout near server hold
WebSocketBidirectional framesSee 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-vus equals concurrent open streams—tune STREAM_VUS to expected live viewers, not REST RPS.
  • Extended timeout on http.get—120s example; align with server long-poll hold + margin.
  • Accept: text/event-stream header—routes through same path browsers use.
  • Tags protocol:sse separate 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

PatternLoad modelPrimary metric
SSE pushMany concurrent open GETsConcurrent VUs + stream errors
Long-pollRepeated long GETs per VUCycle time + timeout rate
REST + SSE mixedSeparate scenariosDo not merge p95
Fan-out backendAdd downstream mock or monitorBroker/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

  1. Import stream endpoint from Postman—headers including Accept. Problem solved: same path QA uses functionally becomes load target.
  2. Set VU scenario + long timeout in UI—120s or per runbook. Problem solved: avoids hand-editing timeout strings across iterations.
  3. Run staged ramp on VUs—50, 100, 200 with abort tripwires. Problem solved: find knee before full-scale rehearsal.
  4. Export timeout and connection errors filtered by protocol:sse. Problem solved: networking team gets status code breakdown, not generic "slow."
  5. Share export with platform—FD chart screenshot same timestamp. Problem solved: capacity ticket links app test to infra evidence.
  6. Save template for release—retag protocol:sse per 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.

Try Performate free | Book a demo | k6 HTTP

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.

← Back to all posts