---
title: "SSE and Long-Polling Under Load: A Practical k6 Testing Pattern"
description: "Load-test SSE and long-polling: concurrent streams, proxy idle timeouts, and k6 patterns—MDN, HTTP/1.1 behavior, and comparison with WebSockets."
publishDate: "2026-06-29"
draft: false
keyword: "server-sent events load testing"
intent: "MOFU"
cta: "Use Performate to turn this playbook into runnable k6 scenarios, thresholds, and shareable reports without losing days to glue code."
tags: ["k6","load-testing","api-performance","server-sent","events"]
---

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](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), [HTTP keep-alive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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](/en/blog/k6-websocket-load-testing-connections).

## 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](/en/blog/k6-websocket-load-testing-connections); 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.

```javascript
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.

```bash
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](/en/blog/k6-websocket-load-testing-connections) 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](https://performate.app) | [Book a demo](/demo) | [k6 HTTP](https://k6.io/docs/using-k6/http-requests/)
