Resource library

QA How-To

k6 Test Server Sent Events Streams: Load Testing Tutorial

Learn how to k6 test server sent events streams with a runnable extension, realistic load stages, message checks, metrics, thresholds, and debugging.

18 min read | 2,762 words

TL;DR

Build k6 with the xk6-sse extension, subscribe with one bounded stream per virtual user, record stream-specific custom metrics, and enforce thresholds for connection failures, first-event latency, message gaps, and payload validity. Test in stages and confirm the server, proxies, and observability stack all see the expected open connections.

Key Takeaways

  • Use the community xk6-sse extension because standard HTTP helpers do not expose events from a long-lived stream.
  • Measure connection success, time to first event, event gaps, stream duration, message count, and malformed payloads.
  • Give each virtual user a bounded stream lifetime so iterations finish and staged load remains understandable.
  • Validate event names, IDs, JSON payloads, and business-level completion signals inside the message handler.
  • Use thresholds that express user-facing reliability goals instead of relying on request duration alone.
  • Correlate k6 results with server connection counts, proxy timeouts, CPU, memory, and downstream saturation.

To k6 test server sent events streams, use an SSE-capable xk6 extension, keep each virtual-user connection open for a controlled interval, and measure events rather than treating the stream like a normal request. The useful signals are connection success, time to first event, gaps between events, message validity, disconnects, and server resource use.

This tutorial builds a local SSE service and a runnable k6 test that increases concurrent streams in stages. For the broader strategy behind scenarios, thresholds, and result analysis, read the k6 Performance Engineering Complete Guide.

You will test protocol behavior and business behavior together. That distinction matters because an HTTP 200 response can remain open while events arrive late, malformed, duplicated, or not at all.

What You Will Build

By the end, you will have:

  • A small Node.js SSE endpoint that emits typed JSON events with monotonic IDs.
  • A custom k6 binary that can subscribe to text/event-stream responses.
  • A staged load scenario that opens one stream per virtual user.
  • Metrics for connection failures, time to first event, event gaps, stream duration, received messages, and invalid messages.
  • Thresholds that turn streaming service-level objectives into a passing or failing test.

The final test is intentionally bounded. Production SSE clients may reconnect and stay online for hours, but a repeatable load test needs a known connection lifetime and a controlled arrival pattern. You can lengthen the stream after the short test is stable.

Prerequisites

Install Node.js 20.19 or newer, Go 1.24 or newer, and Git 2.40 or newer. Use a current k6-compatible xk6 builder rather than assuming the stock k6 binary contains SSE support. Confirm the tools first:

node --version
go version
git --version

Install the xk6 builder:

go install go.k6.io/xk6/cmd/xk6@latest

Make sure the Go binary directory is on PATH. On many systems it is $(go env GOPATH)/bin. You also need two terminals: one for the sample server and one for k6. Docker is optional.

This tutorial uses github.com/phymbert/xk6-sse, whose JavaScript module is imported as k6/x/sse. Pin an extension release in a controlled CI environment after you validate compatibility with your chosen k6 release. The commands below use the current compatible module resolution rather than inventing a version number.

Step 1: Create a Predictable SSE Test Server

Create an empty working directory, initialize Node, and save this as server.mjs:

import http from 'node:http';
import { randomUUID } from 'node:crypto';

const port = Number(process.env.PORT || 3000);

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (url.pathname === '/health') {
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok' }));
    return;
  }

  if (url.pathname !== '/events') {
    res.writeHead(404);
    res.end();
    return;
  }

  const clientId = url.searchParams.get('clientId') || randomUUID();
  const maxEvents = Number(url.searchParams.get('maxEvents') || 20);
  let sequence = 0;

  res.writeHead(200, {
    'content-type': 'text/event-stream',
    'cache-control': 'no-cache, no-transform',
    connection: 'keep-alive',
    'x-accel-buffering': 'no',
  });
  res.flushHeaders();
  res.write(': connected\n\n');

  const timer = setInterval(() => {
    sequence += 1;
    const payload = JSON.stringify({
      clientId,
      sequence,
      sentAt: Date.now(),
    });

    res.write(`id: ${clientId}-${sequence}\n`);
    res.write('event: update\n');
    res.write(`data: ${payload}\n\n`);

    if (sequence >= maxEvents) {
      res.write('event: complete\n');
      res.write(`data: ${JSON.stringify({ clientId, sequence })}\n\n`);
      clearInterval(timer);
      res.end();
    }
  }, 250);

  req.on('close', () => clearInterval(timer));
});

server.listen(port, () => {
  console.log(`SSE server listening on http://localhost:${port}`);
});

The server sends one update every 250 milliseconds, followed by complete. Event IDs and sequence numbers make missing or reordered messages visible. The no-transform and x-accel-buffering headers reduce accidental proxy buffering when you later place the endpoint behind infrastructure.

Verify: Run node server.mjs, then in another terminal run:

curl -N 'http://localhost:3000/events?clientId=manual&maxEvents=3'

You should see a comment, three update events with IDs, and one complete event. Output should arrive incrementally, not all at once.

Step 2: Install k6 with SSE Support

Build a reproducible local binary containing the extension version used by this tutorial:

xk6 build v1.2.2 --with github.com/phymbert/xk6-sse@v0.1.12 --output ./k6-sse
./k6-sse version

An xk6 build combines the k6 core with a Go extension. This keeps the test script close to normal k6 JavaScript while adding an API that understands SSE framing and message callbacks. Treat the resulting binary as a test dependency. Store its build command and extension version in your repository or build it in a pinned container image.

Do not silently substitute http.get() for an SSE client. A normal HTTP call waits for the response body to finish and reports request timing, but it does not expose each event as it arrives. That hides first-event latency, gaps, event types, IDs, and streaming payload errors.

Verify: Save this as smoke.js:

import sse from 'k6/x/sse';

export default function () {
  sse.open('http://localhost:3000/events?clientId=smoke&maxEvents=2', {},
    (client) => {
      client.on('event', (event) => console.log(event.name, event.data));
    });
}

Run ./k6-sse run --vus 1 --iterations 1 smoke.js. The console should show update payloads and a completion message. If the import cannot be resolved, confirm you ran ./k6-sse and that its build includes the pinned extension.

Step 3: Define Stream Metrics and Thresholds

Save the following beginning of the production test as sse-load.js:

import exec from 'k6/execution';
import { check } from 'k6';
import { Counter, Rate, Trend } from 'k6/metrics';
import sse from 'k6/x/sse';

const connectionFailures = new Rate('sse_connection_failures');
const invalidMessages = new Rate('sse_invalid_messages');
const messagesReceived = new Counter('sse_messages_received');
const timeToFirstEvent = new Trend('sse_time_to_first_event', true);
const eventGap = new Trend('sse_event_gap', true);
const streamDuration = new Trend('sse_stream_duration', true);

export const options = {
  thresholds: {
    sse_connection_failures: ['rate<0.01'],
    sse_invalid_messages: ['rate<0.01'],
    sse_time_to_first_event: ['p(95)<1000'],
    sse_event_gap: ['p(95)<750'],
    checks: ['rate>0.99'],
  },
};

Rate values record a boolean on each call, so add both successful and failed samples. Counter records throughput. Trend records distributions in milliseconds, and the second argument marks the custom trend as time. The thresholds here are tutorial defaults based on this local server, not universal production objectives. Replace them with limits derived from user needs and service behavior.

Metric What it reveals Why HTTP duration is insufficient
Time to first event Initial usefulness after subscribing Headers can arrive before useful data
Event gap Delay or stalls during a healthy-looking stream One total duration hides pauses
Invalid message rate Schema and sequence failures HTTP 200 says nothing about payload quality
Stream duration Early disconnects or excessive lifetime Long responses are expected for SSE
Connection failure rate Rejected or broken subscriptions Must be separated from event-level errors

Verify: Run ./k6-sse inspect sse-load.js. k6 should parse the module and list the thresholds without an unknown-module error. No network request runs during inspection.

Step 4: k6 Test Server Sent Events Streams and Validate Every Event

Append this code to sse-load.js:

const baseUrl = __ENV.BASE_URL || 'http://localhost:3000';
const eventsPerStream = Number(__ENV.EVENTS_PER_STREAM || 20);

export default function () {
  const clientId = `vu-${exec.vu.idInTest}-iter-${exec.vu.iterationInScenario}`;
  const url = `${baseUrl}/events?clientId=${encodeURIComponent(clientId)}` +
    `&maxEvents=${eventsPerStream}`;
  const startedAt = Date.now();
  let firstEventAt = 0;
  let previousEventAt = 0;
  let expectedSequence = 1;
  let updates = 0;
  let completed = false;
  let opened = false;

  const response = sse.open(url, {
    headers: { Accept: 'text/event-stream' },
  }, (client) => {
    client.on('open', () => {
      opened = true;
    });

    client.on('event', (event) => {
      const now = Date.now();

      if (!firstEventAt) {
        firstEventAt = now;
        timeToFirstEvent.add(now - startedAt);
      }
      if (previousEventAt) eventGap.add(now - previousEventAt);
      previousEventAt = now;
      messagesReceived.add(1);

      if (event.name === 'update') {
        let payload;
        try {
          payload = JSON.parse(event.data);
        } catch (_) {
          invalidMessages.add(true);
          return;
        }

        const valid = payload.clientId === clientId &&
          payload.sequence === expectedSequence &&
          Number.isFinite(payload.sentAt);
        invalidMessages.add(!valid);
        check(payload, {
          'update payload and sequence are valid': () => valid,
        });
        if (valid) expectedSequence += 1;
        updates += 1;
      }

      if (event.name === 'complete') completed = true;
    });
  });

  connectionFailures.add(!opened || !response || response.status !== 200);
  streamDuration.add(Date.now() - startedAt);
  check(response, {
    'SSE connection returned 200': (r) => Boolean(r) && r.status === 200,
    'all updates arrived': () => updates === eventsPerStream,
    'completion event arrived': () => completed,
  });
}

The callback registers before events are consumed. Each virtual user receives a unique client ID, which prevents test data from becoming ambiguous in logs. The sequence assertion catches dropped, duplicated, or reordered updates. The completion assertion distinguishes an intentional server close from a truncated connection.

This endpoint closes after the requested event count, so sse.open() returns. For a never-ending production endpoint, provide a test-only close condition, use server support for a bounded subscription, or choose an extension API that lets the callback close the client. Never create an infinite iteration that prevents the executor from completing gracefully.

Verify: Run:

./k6-sse run -e EVENTS_PER_STREAM=5 --vus 1 --iterations 1 sse-load.js

Expect five successful update checks, one successful connection check, one completion check, zero invalid messages, and roughly 250 millisecond event gaps. The completion event also counts in sse_messages_received.

Step 5: Add a Realistic Staged Load Model

Replace the options object with this staged scenario and keep the thresholds:

export const options = {
  scenarios: {
    sse_streams: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '20s', target: 10 },
        { duration: '40s', target: 10 },
        { duration: '20s', target: 25 },
        { duration: '40s', target: 25 },
        { duration: '20s', target: 0 },
      ],
      gracefulRampDown: '10s',
    },
  },
  thresholds: {
    sse_connection_failures: ['rate<0.01'],
    sse_invalid_messages: ['rate<0.01'],
    sse_time_to_first_event: ['p(95)<1000'],
    sse_event_gap: ['p(95)<750'],
    checks: ['rate>0.99'],
  },
};

A ramping-VUs executor fits this bounded example because each VU repeatedly opens a stream, receives 20 updates over about five seconds, and starts another iteration while its stage remains active. The active stream count approaches the target VU count. For long-lived streams, the target VUs more directly represent concurrent connections because each VU stays occupied.

Do not confuse stream creation rate with concurrent streams. If the product requirement says 10,000 always-connected clients, model concurrency and connection lifetime. If it says 500 new subscriptions per second during a live event, model arrival rate and ensure the server closes or the test explicitly manages growing concurrency.

Verify: Run ./k6-sse run sse-load.js. During plateaus, inspect the vus value and server-side open-connection metric. Both should settle near the stage target, allowing for connections that close and reopen between samples. All thresholds should pass against the local server on an unconstrained machine.

Step 6: Add Authentication, Reconnection, and Last-Event-ID Coverage

Most production streams require authentication and resumability. Pass a token through the environment instead of hard-coding it:

function streamParams() {
  const headers = { Accept: 'text/event-stream' };
  if (__ENV.ACCESS_TOKEN) {
    headers.Authorization = `Bearer ${__ENV.ACCESS_TOKEN}`;
  }
  if (__ENV.LAST_EVENT_ID) {
    headers['Last-Event-ID'] = __ENV.LAST_EVENT_ID;
  }
  return {
    method: 'GET',
    headers,
    tags: { endpoint: 'events', protocol: 'sse' },
  };
}

Paste streamParams() above the default function from Step 4, then replace its inline params object with streamParams(). The complete Step 4 event handler remains unchanged. Generate or provision tokens outside the timed iteration when authentication is not part of the workload. If token issuance is part of the user journey, put it in setup() or a separate scenario with its own thresholds and tags.

SSE clients commonly reconnect after interruption and send Last-Event-ID. Test that as a separate scenario: capture a known event ID from seeded data, reconnect with that header, and assert the next ID follows it. Do not mix deliberate disconnects into the baseline scenario because they make infrastructure failures harder to interpret. Also verify expired tokens produce a prompt 401 or 403 rather than an open connection with silent missing events.

Browser-native EventSource cannot set arbitrary authorization headers. If the real frontend uses cookies or a signed query parameter, make the load test use the same mechanism. For client-side performance and Web Vitals around a streaming UI, pair this protocol test with the k6 browser Web Vitals testing tutorial.

Verify: Run a one-iteration test with a valid token, then with a deliberately invalid token. Confirm the valid stream receives its completion event and the invalid request increments sse_connection_failures and fails the status check. Never print live tokens in logs.

Step 7: Observe the Server While k6 Runs

A passing client summary does not prove the system has safe capacity. During each stage, collect server and infrastructure signals:

  • Current and accepted SSE connections, disconnect reasons, and reconnect rate.
  • CPU, memory, event-loop lag, file descriptors, sockets, and garbage collection.
  • Proxy upstream response buffering and idle timeout behavior.
  • Publisher-to-client delivery delay, queue depth, and dropped event count.
  • Load balancer distribution and per-instance connections.
  • Database, broker, cache, and authorization-service saturation.

Tag the scenario, endpoint, and environment so exported results remain queryable. Avoid high-cardinality tags such as raw client IDs. Those belong in sampled logs, not metric labels. If you need distributed tracing to explain slow first events or widening gaps, follow the k6 OpenTelemetry trace export tutorial. Remember that long-lived connections may create very long spans, so define a sampling and span strategy before a large run.

For large concurrency, one load generator can become the bottleneck. Watch its CPU, memory, sockets, and outbound bandwidth. Run a no-op or simplified endpoint test to estimate generator overhead, then distribute execution only after the script is correct. The k6 distributed load testing with the Kubernetes Operator guide shows how to scale generators without losing scenario consistency.

Verify: At the 10-VU plateau, compare k6 vus with the server's active-connection count. At the 25-VU plateau, confirm resources rise as expected and return toward baseline after ramp-down. A mismatch usually indicates fast disconnects, buffering, a metric scrape issue, or load-generator exhaustion.

Step 8: Run a Controlled Capacity Experiment

Start with a smoke run, then a baseline, then a step test. Change only one major variable per experiment. Keep event size, frequency, connection lifetime, authentication behavior, and server deployment constant while increasing concurrency.

Use a sequence like this:

# Smoke: protocol and assertions
./k6-sse run --vus 1 --iterations 1 -e EVENTS_PER_STREAM=5 sse-load.js

# Local staged baseline
./k6-sse run -e BASE_URL=http://localhost:3000 sse-load.js

# Environment run, token supplied by the shell or secret store
./k6-sse run -e BASE_URL=https://test.example.com \
  -e ACCESS_TOKEN=REDACTED sse-load.js

For the protected endpoint, use your CI secret injection instead of the literal placeholder. Save the k6 summary, deployment version, environment configuration, data volume, and server dashboards for each run. Capacity is a property of a specific system configuration and workload, not a permanent number.

Stop increasing load when user-facing thresholds fail, server safety limits are approached, or the generator saturates. Then repeat the last passing and first failing stages to confirm the boundary. A single run can be distorted by cold caches, noisy neighbors, autoscaling delay, or an unrelated downstream incident.

Verify: The smoke run must have zero invalid messages and all three checks passing. The staged run must report threshold status for both protocol and message metrics. Confirm the test environment received the same number and shape of events expected from its publisher.

Troubleshooting

Events arrive in one large batch -> Disable response buffering in the reverse proxy and compression middleware for text/event-stream. Confirm with curl -N before blaming k6. Nginx configurations often need buffering disabled for the SSE location, while application code must flush headers and each event boundary.

The script never finishes -> The production stream never closes. Add a bounded test mode, a maximum event count, or a supported client-close call. Ensure graceful stop behavior is tested separately so an endless stream does not trap every VU.

k6/x/sse cannot be resolved -> Run the pinned xk6-built binary so local and CI behavior match. Execute ./k6-sse version and rebuild with the extension if the artifact was replaced.

Connections close at the same fixed interval -> Inspect load balancer, ingress, CDN, service mesh, and application idle timeouts. Send SSE comment heartbeats frequently enough for the real infrastructure policy, but do not treat a heartbeat as a business event.

First-event latency is good but gaps grow under load -> Inspect event-loop lag, broker consumers, per-client queues, locks, serialization, and network backpressure. Header timing alone will miss this degradation.

k6 and server connection counts disagree -> Check metric scrape timing, rapid reconnects, NAT or proxy pooling assumptions, failed handshakes, and generator limits. Compare a small plateau manually before scaling out.

Common Mistakes and Best Practices

Do not judge SSE performance from http_req_duration alone. A long duration is normal for a stream. Add event-aware metrics and assert the completion behavior expected by the scenario.

Do not open thousands of connections immediately. Ramp gradually, hold plateaus long enough for caches and autoscaling to settle, and watch both the system and the generators. Use production-like event frequency and payload size because idle connections and busy streams consume very different resources.

Keep failure experiments separate from the baseline. Test invalid credentials, midstream disconnects, replay with Last-Event-ID, duplicate events, and slow consumers deliberately. Label each scenario so its expected failures do not pollute reliability thresholds.

Finally, protect the environment. Agree on abort conditions, notify operators, isolate test accounts, avoid personal data, and cap the first run conservatively. Distributed SSE tests can create substantial socket, logging, broker, and egress load even when request-per-second charts look quiet.

Where To Go Next

You now have a repeatable protocol-level SSE test. Use the complete k6 performance engineering guide to place it inside a broader workload, threshold, CI, and capacity-planning strategy.

Then deepen the areas your result exposes:

For more foundation, review the k6 load testing tutorial and the k6 thresholds and checks guide.

Before a major test, add reconnection and replay scenarios, verify infrastructure timeouts, and create a dashboard that aligns active connections, event gaps, first-event latency, server saturation, and generator health on one timeline.

Interview Questions and Answers

Q: Why is SSE load testing different from normal HTTP load testing?

An SSE response stays open and delivers multiple framed events over time. Request duration alone cannot describe time to first event, event gaps, message order, malformed payloads, or premature disconnects. A strong test measures both connection behavior and the event stream.

Q: How do you model concurrent SSE users in k6?

Use one active subscription per VU and choose a VU-based executor when the primary target is concurrent connections. Keep the stream lifetime controlled and use staged plateaus. Validate the target against server-side active-connection metrics.

Q: Which SSE metrics matter most?

Track connection failure rate, time to first event, inter-event gaps, message throughput, invalid payload rate, stream duration, reconnects, and lost or duplicated sequence IDs. Correlate those with server sockets, CPU, memory, queues, and publisher-to-client latency.

Q: How should reconnection be tested?

Create a dedicated scenario that interrupts a stream, reconnects with Last-Event-ID, and asserts that delivery resumes according to the product contract. Include expired credentials and retry backoff. Keep planned disconnects separate from the normal reliability baseline.

Q: Why can the load generator become a bottleneck?

Many concurrent sockets consume file descriptors, memory, CPU, and network capacity on the generator. Monitor it like a system component and scale out only after a small test proves the script and endpoint behavior. Otherwise generator saturation can look like server failure.

Q: What proves an SSE test passed?

Passing means the agreed thresholds hold for connections and event delivery while server and generator safety limits remain healthy. It also means expected message counts, schemas, ordering, and completion or reconnection behavior are correct. An HTTP 200 by itself is not enough.

Conclusion: k6 Test Server Sent Events Streams Reliably

To k6 test server sent events streams well, build an SSE-capable k6 binary, model concurrent long-lived connections, and validate every meaningful event. Use first-event latency, event gaps, payload validity, sequence integrity, disconnect behavior, and system telemetry to reveal failures that ordinary request metrics hide.

Start with the local smoke test, establish a stable staged baseline, and add authentication, reconnection, and distributed execution one controlled scenario at a time. That progression gives you a trustworthy capacity result instead of a large but ambiguous connection count.

Interview Questions and Answers

Why does an HTTP 200 not prove an SSE stream is healthy?

The server can send successful headers and then stall, buffer, duplicate, reorder, or corrupt events. SSE health requires event-aware checks such as time to first event, inter-event gaps, schema validity, sequence integrity, and disconnect behavior. HTTP status is only the connection-level starting point.

How would you model 10,000 concurrent SSE clients?

I would use one subscription per VU, ramp through safe plateaus, and hold each plateau long enough to observe steady state. I would monitor the generators and distribute them when needed, while comparing k6 VUs to server-side active connections. Event frequency, payload size, authentication, and reconnect behavior must match the expected workload.

Which custom metrics would you create for an SSE test?

I would add connection failures, time to first event, inter-event gap, stream duration, messages received, invalid payload rate, reconnect count, and missing or duplicate sequence count. If server timestamps are trustworthy, I would add end-to-end event delivery delay. Each metric would have a requirement-based threshold where appropriate.

How do you distinguish server saturation from load-generator saturation?

I monitor CPU, memory, sockets, file descriptors, and bandwidth on both sides. I compare small and distributed runs, review server active connections, and repeat the boundary stage. If adding generators improves delivery while the server remains healthy, the original generator was likely limiting the test.

How would you validate SSE reconnection behavior?

I would create a separate fault scenario, record the last valid event ID, interrupt the connection, and reconnect using Last-Event-ID or the application's equivalent. I would assert replay order, duplicates according to the delivery contract, retry delay, and token behavior. Planned disconnects stay separate from baseline error thresholds.

Why should event frequency and payload size be production-like?

Idle sockets mainly test connection capacity, while frequent large events stress serialization, brokers, queues, CPU, and network egress. A system can support many idle streams but fail at the expected publication rate. Both dimensions belong in the workload model.

What infrastructure problems commonly affect SSE under load?

Common issues include proxy buffering, fixed idle timeouts, insufficient file descriptors, uneven load balancing, downstream queue growth, and generator socket exhaustion. I verify incremental delivery with curl, align timeout policies, watch active connections per instance, and correlate event gaps with system telemetry.

Frequently Asked Questions

Can standard k6 load test Server-Sent Events?

A standard HTTP request can open the endpoint, but it does not provide a convenient event-by-event SSE API. Build k6 with a compatible SSE extension so the script can handle event frames, names, IDs, and payloads as they arrive.

What is the best executor for SSE testing in k6?

Use a VU-based executor such as ramping-vus when your primary load target is concurrent open streams. Use an arrival-rate model only when subscription starts per unit of time are the requirement, and account for the growing concurrency caused by long stream lifetimes.

How do I measure SSE latency?

Measure time from subscription start to the first useful event, plus the gap between later events. If payloads contain a trustworthy server timestamp, also measure publisher-to-client delivery delay, while accounting for clock synchronization.

How many SSE connections can one k6 generator create?

There is no universal safe number because hardware, operating-system limits, TLS, event frequency, payload size, script work, and network capacity all matter. Monitor generator CPU, memory, file descriptors, sockets, and bandwidth, then prove capacity with gradual plateaus.

How do I stop an SSE test from running forever?

Use a test endpoint option that closes after a maximum event count or duration, or use a supported client-close API. A bounded stream lets k6 iterations finish and makes stages, summaries, and graceful shutdown predictable.

Should SSE heartbeat comments count as events?

No. Heartbeat comments keep connections alive but do not represent business delivery. Track heartbeat behavior separately and calculate user-facing latency from typed data events.

How do I test Last-Event-ID and SSE replay?

Disconnect after a known event, reconnect with that ID in the mechanism supported by the real client, and assert the next delivered event follows the replay contract. Run this as a separate scenario so deliberate interruptions do not contaminate baseline thresholds.

Related Guides