QA How-To
k6 Performance Engineering Complete Guide (2026)
Use this k6 performance engineering complete guide 2026 to model realistic load, set thresholds, automate tests, diagnose bottlenecks, and scale safely.
20 min read | 3,934 words
TL;DR
Build k6 tests around a documented workload, semantic checks, risk-based thresholds, and correlated telemetry. Start small, automate a safe gate, and scale from evidence.
Key Takeaways
- Model workloads from evidence.
- Combine checks with thresholds.
- Choose executors from demand behavior.
- Verify generator health and delivered load.
- Correlate client symptoms with backend telemetry.
- Scale only after validating one runner.
The k6 performance engineering complete guide 2026 takes you from a controlled smoke check to a reproducible performance program. You will model traffic, validate correctness, set release thresholds, automate safe runs, and connect client symptoms with system evidence.
k6 is a load generator and measurement tool, not a substitute for workload research or diagnosis. The strongest test states its question, assumptions, environment, stop conditions, and decision rule before it starts.
TL;DR
| Need | Capability | Evidence |
|---|---|---|
| Protocol load | HTTP scenarios | Rate, latency, errors |
| Correctness | Checks | Semantic outcomes |
| Release gate | Thresholds | Pass or fail exit code |
| User experience | Browser module | Rendered journey metrics |
| Diagnosis | Tags and telemetry | Operation and backend cause |
What You Will Build with the k6 Performance Engineering Complete Guide 2026
- A smoke test with correctness checks.
- A staged arrival-rate workload.
- Tagged journeys with bounded test data.
- Risk-based thresholds and a CI gate.
- An observability and scaling decision map.
Prerequisites
Use a maintained k6 release from Grafana's official package channel and Node.js 20 or newer for a local target. Run brew install k6 on macOS or the signed Grafana apt repository on Debian-based systems. Container users can run docker run --rm grafana/k6 version. Pin the validated version in CI.
Confirm k6 version, node --version, and authorized access to a disposable environment. Never direct tutorial load at production or a shared service without explicit approval, monitoring, and stop conditions.
Step 1: Install k6 and Start a Target
Install k6 from the official Grafana package channel, then confirm it with k6 version. Start a disposable target and never load a shared or production service without authorization.
brew install k6
k6 version
node --version
Use Node.js 20 or newer to run a local service with health and products routes. Keep the target build, dataset, infrastructure, and feature flags fixed so later comparisons are meaningful.
Verify: k6 version prints an installed release. A curl request to the approved base URL returns HTTP 200.
Step 2: Create a Smoke Test
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = { vus: 1, iterations: 3, thresholds: { checks: ['rate==1'], http_req_failed: ['rate==0'], http_req_duration: ['p(95)<500'] } };
export default function () {
const r = http.get(__ENV.BASE_URL + '/health', { tags: { operation: 'health' } });
check(r, { 'status is 200': x => x.status === 200, 'body is healthy': x => x.json('status') === 'ok' });
sleep(0.2);
}
Checks record correctness. Thresholds evaluate aggregate metrics and control the exit status. A fast HTTP 500 is not successful performance, so always use both.
Verify: Run BASE_URL=http://127.0.0.1:3000 k6 run smoke.js. Expect three iterations, all checks passing, and a zero exit code. Change the route to a missing path and confirm failure.
Step 3: Model Load with Scenarios
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: { catalog: { executor: 'ramping-arrival-rate', startRate: 2, timeUnit: '1s', preAllocatedVUs: 10, maxVUs: 30, stages: [{ target: 5, duration: '20s' }, { target: 10, duration: '30s' }, { target: 0, duration: '10s' }] } },
thresholds: { 'http_req_duration{operation:products}': ['p(95)<300'], 'http_req_failed{operation:products}': ['rate<0.01'], checks: ['rate>0.99'], dropped_iterations: ['count==0'] }
};
export default function () { const r = http.get(__ENV.BASE_URL + '/products', { tags: { operation: 'products' } }); check(r, { 'catalog works': x => x.status === 200 && Array.isArray(x.json()) }); }
Use arrival-rate executors when external demand should not decrease because the system slows. Use VU executors for a fixed population that waits before looping. Derive rates and transaction mix from analytics or logs, and label unsupported values as hypotheses.
Verify: Run the script and confirm achieved rates, zero dropped iterations, passing checks, and passing thresholds. Dropped iterations mean the intended open workload was not delivered.
Step 4: Add Journeys, Data, and Correlation
Use groups for readable transactions, low-cardinality tags for metric slices, and SharedArray for read-only fixtures parsed once per process. Extract server-generated IDs from responses and pass them to later requests. Allocate independent mutable records per virtual user.
import http from 'k6/http';
import { check, group, sleep } from 'k6';
export default function () {
group('browse catalog', () => {
const r = http.get(__ENV.BASE_URL + '/products', { tags: { journey: 'shopper', operation: 'products' } });
check(r, { 'products are returned': x => x.status === 200 && x.json().length > 0 });
});
sleep(Math.random() * 1.5 + 0.5);
}
Never place unique user IDs, URLs, or request IDs in metric tags because unbounded values create excessive time series. Put those identifiers in sampled logs or traces. Inject secrets from CI and never print them.
Verify: Run two VUs for ten seconds. Confirm the group appears, checks pass, and operation tags are visible in metric output.
Step 5: Define Risk-Based Thresholds
A threshold is an executable objective for a named test context. Gate correctness, errors, critical-operation latency, and delivered workload. A percentile such as p(95)<300 limits most latency but does not describe the slowest tail. Add another percentile only when product risk justifies it.
| Signal | Question | Trap |
|---|---|---|
| Check rate | Was behavior correct? | Transport success mistaken for business success |
| Error rate | Did requests fail? | Incorrect HTTP 200 bodies ignored |
| p(95) | Were most requests responsive? | Slow tail hidden |
| Dropped iterations | Was open load delivered? | Generator shortage blamed on target |
| Group duration | Was the journey timely? | Compared with one request |
Verify: Temporarily set an impossibly strict duration threshold. The run must complete with a nonzero exit code, proving CI can enforce the objective.
Step 6: Diagnose with Observability
Client metrics show symptoms. Server metrics, logs, profiles, and traces explain causes. Align clocks and compare the same steady-state window. Inspect CPU, memory, queues, connection pools, database locks, downstream latency, autoscaling, and generator saturation.
Do not infer causation from one correlated graph. Repeat the breakpoint, change one variable, inspect queueing and recovery, then design the smallest experiment that could disprove the leading explanation. Use the k6 OpenTelemetry export tutorial to connect generated traffic with backend spans.
Verify: Compare a fast health operation with a deliberately slower product operation. Tagged metrics should separate them. If only one operation slows, a global service-latency conclusion is unsupported.
Step 7: Automate a Safe CI Gate
name: k6 smoke
on: [workflow_dispatch]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: grafana/setup-k6-action@v1
- run: k6 run --summary-export=summary.json smoke.js
env:
BASE_URL: ${{ secrets.PERF_BASE_URL }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: k6-summary
path: summary.json
Keep small smoke gates on pull requests. Schedule or manually approve heavier profiles. Store commit, target build, k6 version, dataset, command, and sanitized configuration with results. Preserve the k6 exit code as the gate.
Verify: Run the workflow manually. A passing threshold creates a green job and artifact. Tighten one threshold and confirm the job fails while the artifact still uploads.
Step 8: Select Advanced Techniques
Protocol tests generate efficient load but do not render pages. Use the k6 browser Web Vitals tutorial for rendered experience. Use distributed k6 testing with the Kubernetes Operator only after one runner is validated. Use the k6 SSE stream tutorial for long-lived event assertions.
Distribution introduces coordination, networking, data partitioning, and result aggregation. Browser tests consume more resources. Streams require connection, event, timing, reconnect, and cleanup assertions. Choose from protocol behavior and user risk, not novelty.
Verify: Write a test charter naming the risk, workload source, executor, checks, thresholds, environment, telemetry, owners, and stop conditions. A reviewer should explain why each technique belongs.
9. Design a Defensible Workload Model
A workload model translates product behavior into test inputs. Begin with a fixed observation window from production analytics, gateway logs, or an approved synthetic baseline. Record request rate, concurrent sessions, journey mix, payload distribution, geographic origin, cache state, authentication behavior, and think time. Keep the raw evidence beside the model so another engineer can audit every assumption.
Do not convert monthly traffic directly into a peak second. Aggregate counts hide daily peaks, bursts, retries, and background traffic. Select a representative busy interval, then calculate the rate for each business operation. If evidence is missing, write a range and mark it as a hypothesis. A test that says "search is estimated at 15 to 25 starts per second" is more honest and useful than a precise number with no source.
Closed and open models answer different questions. A closed model uses a fixed population of VUs. When the target slows, those users complete fewer iterations, which resembles people who wait before continuing. An open model starts work at an independent arrival rate. It exposes queue growth because demand does not automatically fall when response time rises.
| Production behavior | k6 executor | Use it when | Watch closely |
|---|---|---|---|
| Fixed looping population | constant-vus | A known group repeats journeys | Iteration duration and pacing |
| Population changes by stage | ramping-vus | Concurrency itself is the experiment | Completed iteration rate |
| Stable external arrivals | constant-arrival-rate | Work arrives independently of latency | Dropped iterations |
| Changing external arrivals | ramping-arrival-rate | You need ramps, peaks, and recovery | VU allocation and delivery |
| Exact finite sample | shared-iterations | You need a bounded batch | Uneven iteration distribution |
Model transaction mix explicitly. If browse is 70 percent, search is 20 percent, and checkout is 10 percent, implement separate scenarios or a documented weighted choice. Separate scenarios are easier to tag, allocate, stop, and threshold. Weighted branching can represent one user population, but it needs enough iterations for the observed mix to stabilize.
Pacing is part of the workload, not decorative sleep. Use measured think-time distributions where possible. Avoid a single identical delay for every user because synchronized loops create artificial waves. Keep a zero-pacing capacity profile separate from the realistic user profile, and label its purpose clearly.
Calculate Little's Law Carefully
For a stable system, approximate concurrency as arrival rate multiplied by average time in the system. Ten iterations per second with a two-second average journey implies about twenty active iterations. This is a planning estimate, not a VU setting guarantee. Iteration code, sleep, authentication, data setup, redirects, and target slowdown all affect the VUs needed to sustain an arrival rate. Set preAllocatedVUs from a measured control run, then allow a bounded maxVUs with monitoring.
Document the target rate, stage duration, expected mix, pacing, and maximum generated data before execution. Also document stop conditions such as error spikes, exhausted database connections, or generator saturation. This turns a script into an approved experiment.
10. Build Reliable Test Data and Lifecycle Hooks
Test data failures often look like performance failures. Shared accounts collide, reused carts grow without bounds, expired tokens create error waves, and cleanup jobs compete with the workload. Design data ownership before raising concurrency.
Use setup() for bounded initialization that runs once and returns serializable data to VUs. Use teardown() for best-effort cleanup or reporting. Do not depend on teardown as the only cleanup path because an interrupted process may not reach it. Prefer disposable tenants, records with a run identifier, and a separate idempotent cleanup command.
Read-only fixtures can use SharedArray so each VU does not parse a duplicate copy. Mutable state must remain isolated. Partition accounts deterministically with exec.vu.idInTest, or lease records from an approved test-data service. Ensure the pool is larger than the maximum active VUs and that each record can be reset.
Tokens need a deliberate strategy. A single token can create unrealistic authorization-cache behavior or server-side contention. Per-user login can overload identity services and hide the system under test. Choose among pre-issued tokens, a separate authentication scenario, or controlled login based on the question. Never write credentials, tokens, or personal data to console output or artifacts.
Correlation means extracting a value produced by one response and using it in the next request. Check the producing response before reading its fields. If creation fails, stop that iteration rather than sending malformed downstream requests that pollute latency and error metrics. Use fail() for a condition that makes the whole test invalid, and use checks plus an early return for an isolated journey failure.
Data volume must be bounded. Estimate records created per second multiplied by test duration, add retry behavior, and verify storage capacity. For a soak test, include retention jobs and database growth in monitoring. A stable response percentile while a table grows rapidly can still reveal an operational failure waiting to happen.
Protect Metric Cardinality
Tags should describe a small, stable set such as operation, journey, expected status class, or API version. Do not tag metrics with email addresses, product IDs, trace IDs, raw URLs, or randomly generated record IDs. Every unique combination can become a separate time series in an output backend.
If you need request-level correlation, add an approved test-run header and capture identifiers in sampled logs or traces. Keep the metric tag at the run or operation level. This preserves diagnostic power without overwhelming storage and query systems.
11. Run Capacity, Stress, Spike, and Soak Experiments
One script should not pretend to answer every performance question. Reuse trusted journey functions, but give each experiment its own scenario profile, thresholds, duration, monitoring plan, and interpretation.
Baseline and Load
Start with a smoke run that proves routing, data, checks, and credentials. Follow it with a low-load baseline on an otherwise quiet environment. The baseline reveals fixed latency, connection setup, cache behavior, and monitoring gaps. A normal load run then applies the expected busy workload long enough to pass warm-up and observe a stable measurement window.
Do not include warm-up blindly in the final percentile. Inspect time-series output and state which interval supports the conclusion. Warm-up is real behavior if users encounter it after deployments or scale-to-zero events, but it answers a different question from steady-state latency.
Capacity and Stress
A capacity test raises demand in controlled stages until an objective is approached. Hold each stage long enough for queues, caches, garbage collection, autoscaling, and database pools to react. The last passing stage is evidence for that environment and workload, not a permanent capacity number.
A stress test continues beyond an expected operating range to discover the first constraint and failure shape. Define a ceiling and stop conditions before starting. Observe whether errors are bounded, whether timeouts align across layers, and whether the service sheds load intentionally. A graceful 429 response can be healthier than slow requests that consume every worker.
Spike and Recovery
A spike test changes arrivals quickly. It evaluates admission control, queue limits, autoscaling lag, cache stampedes, and retry amplification. Measure recovery after the spike, not only the peak. Verify that error rate, queue depth, replica count, memory, and latency return toward the baseline instead of remaining degraded.
Client retries can multiply a spike. Model only retries that real clients perform, including their limit, backoff, and jitter. Do not add immediate infinite retries to make checks pass. Report both logical operations and physical requests so stakeholders can see amplification.
Soak
A soak test holds a realistic rate long enough to expose accumulation: memory leaks, unclosed connections, log growth, token expiry, data growth, scheduled jobs, and gradual queue drift. Reduce generator logging and confirm artifact storage before a long run. Use periodic health notes and a pre-agreed abort procedure.
Compare early, middle, and late steady windows. A flat overall percentile can hide a slow trend. Examine memory, heap behavior, connection counts, database size, background compaction, and generator resources. After load stops, keep observing recovery and cleanup.
12. Interpret k6 Metrics Without False Conclusions
k6 reports measurements from the load generator's point of view. http_req_duration covers sending, waiting, and receiving, but excludes time spent acquiring a free connection before the request. Breakdowns such as blocked, connecting, TLS handshaking, sending, waiting, and receiving help distinguish network setup from server processing. Interpret them with server telemetry rather than renaming waiting time as backend execution time.
Percentiles describe a distribution. The p95 value means 95 percent of observed values were at or below that boundary. It does not mean every user was fast, nor does it identify why the slow tail occurred. Always pair latency with sample count, correctness, errors, achieved rate, stage, and operation tags.
Aggregate metrics can create Simpson's paradox. A faster high-volume health route may hide a slower checkout route. Threshold critical operations with tag selectors and show their distributions separately. Keep operation names stable across releases so trend comparisons remain meaningful.
Checks are rates of boolean outcomes, not assertions that automatically stop execution. A failed check increments a metric and the VU continues unless the script changes control flow. Add a threshold on checks or on a tagged check group when correctness must fail the run.
http_req_failed follows k6's expected-response behavior. Business failures returned with HTTP 200 need semantic checks or custom metrics. Conversely, a deliberately expected 404 may need response callback handling if it should not count as an HTTP failure. Write the expected behavior in the test charter.
Dropped iterations in arrival-rate scenarios indicate that k6 could not start intended iterations. Causes include insufficient VU allocation, unexpectedly long iterations, or generator limits. They mean delivered demand differs from requested demand. Treat that as a test validity issue before judging target capacity.
Trend comparisons require comparable conditions. Preserve the target commit, infrastructure shape, region, dataset, feature flags, dependency versions, k6 version, script commit, and output configuration. Repeat a surprising result. Use several controlled runs when normal variance could reverse the decision, and never claim statistical certainty from one convenient run.
13. Correlate Results and Diagnose Bottlenecks
Begin diagnosis at the first stage where a user-facing objective changes. Align k6 time series with service metrics using synchronized clocks and a shared run identifier. Compare the same operation, stage, and interval. A dashboard showing the entire hour can conceal the breakpoint.
Follow a resource and queue checklist. Inspect CPU saturation and throttling, memory pressure and garbage collection, thread or worker pools, event-loop lag, connection pools, request queues, database locks, query plans, cache hit rate, downstream latency, network errors, and autoscaling events. Inspect the generator with the same discipline.
High CPU near the latency breakpoint is a clue, not proof. It may be useful work, retries, serialization, compression, garbage collection, or another tenant. Form a falsifiable hypothesis such as "JSON serialization saturates one worker per core after 80 search starts per second." Change one variable, repeat the stage, and seek evidence that would reject the claim.
Queueing explains many nonlinear failures. Once arrival rate approaches service rate, waiting time can rise sharply even when individual processing time changes little. Look for growing in-flight work, pool wait time, queue depth, and timeouts. Raising a timeout can move the failure downstream and consume more resources without increasing throughput.
Distributed traces are most useful when sampling and correlation are planned. Tag the test traffic with a low-cardinality run value, preserve operation names, and examine representative slow and failed traces. The k6 OpenTelemetry export tutorial shows the deeper workflow. Avoid tracing every request at extreme load unless the telemetry system is intentionally part of the experiment.
Profiles can confirm where CPU or allocation time is spent, but profiling has overhead. Capture a bounded window around a reproducible stage. Compare it with a baseline profile, and record profiler settings in the evidence package.
A good diagnosis separates observation, inference, and conclusion. "Checkout p95 rose after 40 starts per second" is an observation. "Database pool waiting rose in the same window" is correlated evidence. "The pool is the limiting cause" remains a hypothesis until a controlled change shifts the breakpoint as predicted.
14. Establish a Performance Engineering Operating Model
Performance testing becomes engineering when it produces repeatable decisions throughout delivery. Store scripts beside application code or in a versioned repository with review ownership. Treat workload models, thresholds, fixtures, dashboards, and environment definitions as code. A script change can alter the meaning of a trend as much as an application change.
Use a test portfolio instead of one oversized gate. Run a tiny semantic smoke test on pull requests. Run a short, stable regression profile after deployment to a controlled environment. Schedule longer load and soak tests when the environment is quiet and monitored. Require explicit approval for stress, spike, distributed, or production-like tests.
Threshold ownership matters. Product teams define critical journeys and acceptable experience. Service owners define reliability constraints and operational limits. Performance engineers translate those expectations into measurable test contexts and challenge unsupported assumptions. No one should silently loosen a failing threshold just to restore a green pipeline.
Create a lightweight result record for every decision-making run:
- Question and risk being tested.
- Application and test commits.
- Environment topology, dataset, and feature flags.
- Workload stages, transaction mix, and pacing.
- Requested and achieved demand.
- Checks, thresholds, and stop conditions.
- Generator and target health.
- Key graphs, traces, logs, and anomalies.
- Conclusion, limitations, owner, and next experiment.
Baseline drift requires review. Dependencies change, datasets grow, cloud placement varies, and observability adds overhead. Use trends to detect change, but compare only compatible runs. When the environment changes intentionally, establish a new baseline and retain the reason.
Define an escalation path before a large run. Name the person who can stop the test, the service owner watching telemetry, and the channel for incident communication. Avoid real customer data, uncontrolled third-party calls, and production notifications. Confirm rate limits and cost boundaries for every dependency in scope.
This k6 performance engineering complete guide 2026 is most valuable as a feedback loop. Begin with evidence, execute a bounded experiment, validate delivery and correctness, correlate symptoms, test the leading explanation, and update the model. The artifact is not merely a chart. It is a defensible decision with enough context to reproduce or challenge it.
The Complete Series
- Measure browser Web Vitals with k6: Add rendered journeys and browser experience metrics.
- Distribute k6 tests with the Kubernetes Operator: Scale a validated workload across Kubernetes runners.
- Test Server-Sent Events with k6: Verify stream events, timing, reconnects, and cleanup.
- Export k6 traces with OpenTelemetry: Correlate load activity with backend spans.
Troubleshooting
Connection refused -> Start the target and test the base URL from the same runner. Inside a container, localhost refers to that container.
Checks fail but HTTP errors do not -> Transport succeeded but business content was wrong. Add a threshold on checks.
Thresholds fail unexpectedly -> Inspect the exact metric selector, operation tags, percentile, warm-up, and steady window before changing the objective.
Dropped iterations increase -> Check preallocated VUs, maximum VUs, iteration duration, and generator saturation.
Runs vary widely -> Control target build, dataset, cache, autoscaling, background work, and measurement windows.
Generator memory grows -> Bound fixture data and response retention, avoid high-cardinality tags, and inspect the runner itself.
Where To Go Next
Replace tutorial rates with observed journeys and review the model with development, operations, product, and security stakeholders. Add backend dashboards before raising load. Then choose browser Web Vitals, Kubernetes distribution, SSE validation, or OpenTelemetry tracing. Related foundations include test data strategy and Kubernetes basics for testers.
Interview Questions and Answers
Q: How do checks differ from thresholds? Checks evaluate individual correctness conditions. Thresholds evaluate aggregate metrics and control exit status.
Q: When should you use arrival rate? Use it when externally arriving demand should remain independent of system slowdown. Monitor dropped iterations and generator health.
Q: How do you choose thresholds? Derive them from user expectations and service objectives for a named workload and environment. Gate correctness, errors, latency, and delivery.
Q: Why are averages inadequate? They hide distribution shape and slow tails. Inspect percentiles, operation tags, errors, and time windows.
Q: How do you validate the generator? Monitor CPU, memory, network, sockets, achieved rate, and dropped iterations. Compare against a trivial-target baseline.
Q: How do you isolate data? Allocate independent identities and records, clean them deterministically, inject secrets, and keep unique IDs out of metric tags.
Q: What belongs in a report? Include question, build, environment, data, workload, achieved load, thresholds, telemetry, anomalies, limitations, and next experiments.
Best Practices
- Obtain authorization and define stop conditions.
- Model demand from evidence and label hypotheses.
- Validate correctness before speed.
- Gate critical operations with tagged thresholds.
- Separate smoke, load, stress, spike, and soak purposes.
- Verify intended load was delivered.
- Correlate client metrics with backend telemetry.
- Preserve configuration and results for reproduction.
Common Mistakes
- Picking virtual users because the number looks impressive.
- Reporting only averages or request counts.
- Ignoring generator saturation and dropped iterations.
- Sharing mutable accounts across VUs.
- Raising a failed threshold without diagnosis.
- Scaling distribution before validating one runner.
- Treating staging evidence as a production guarantee.
k6 Performance Engineering Complete Guide 2026 Conclusion
This k6 performance engineering complete guide 2026 gives you a working loop: model risk, check correctness, shape traffic, gate objectives, preserve evidence, and diagnose with telemetry. Run the smoke example, force one threshold failure, explain it from both client and server evidence, then replace tutorial assumptions with observed product behavior.
Interview Questions and Answers
How do checks differ from thresholds?
Checks evaluate individual responses or business conditions and emit a rate, but they do not fail a run by themselves. Thresholds evaluate an aggregate metric over the run and produce a nonzero exit code when the objective is missed. I use checks to prove semantic correctness, then place thresholds on checks, error rate, latency, and delivered load so CI can enforce the decision.
When do you use an arrival-rate executor?
I use an arrival-rate executor when work arrives independently of response time, such as API requests, messages, or a public traffic peak. It keeps the requested start rate from falling automatically when the system slows. I size preallocated VUs from a control run and monitor dropped iterations, achieved rate, and generator health so I know whether the intended workload was delivered.
How do you validate the load generator?
I monitor generator CPU, memory, network, sockets, and k6 process health throughout the run. For open models I compare requested starts with achieved iterations and treat dropped iterations as a validity warning. I also run a known fast target or low-load baseline to separate generator overhead from target behavior before scaling out.
How do you choose thresholds?
I derive thresholds from a named user journey, workload, environment, and business or service objective. I gate semantic checks, HTTP failures, critical-operation percentiles, and load delivery rather than using one global latency number. I validate each selector by forcing a controlled failure, and I never relax a threshold without documenting why the objective changed.
Why is average latency misleading?
An average can remain acceptable while a meaningful minority of users experiences severe delay, and a high-volume fast route can hide a slow critical route. I inspect operation-level percentiles, sample counts, errors, and time-series behavior for each stage. I also correlate the slow tail with queues and backend telemetry instead of treating a percentile as a root cause.
How do you isolate performance test data?
I allocate independent accounts or records per VU or scenario, usually with deterministic partitioning or a lease service. Read-only fixtures can be shared, but mutable records cannot. I use run identifiers and idempotent cleanup, keep secrets out of logs, and avoid unique identifiers in metric tags because they create high cardinality.
What belongs in a performance report?
I report the question, application and test versions, environment, dataset, workload model, requested and achieved load, checks, thresholds, and generator health. I include operation-level results and the backend evidence that supports the diagnosis. I separate observations from hypotheses, state limitations, and finish with the decision, owner, and smallest useful next experiment.
Frequently Asked Questions
Is k6 suitable for performance engineering in 2026?
Yes. k6 supports protocol, browser, scenarios, thresholds, CI, and observability workflows, while sound engineering still requires realistic models and diagnosis.
What is the difference between checks and thresholds?
Checks record individual correctness. Thresholds evaluate aggregate metrics and determine exit status.
How many virtual users should I use?
Derive the count from active users, pacing, transaction mix, and concurrency rather than choosing an arbitrary value.
Should I use constant VUs or arrival rate?
Use constant VUs for a fixed looping population and arrival rate for externally arriving work that should not slow with the target.
How does k6 fail a CI job?
Define thresholds. A failed threshold produces a nonzero exit code that CI can enforce.
Can k6 measure browser Web Vitals?
Yes. Use the browser module for rendered journeys and Web Vitals, typically at smaller concurrency than protocol tests.
When should I distribute k6 tests?
Distribute only after one generator cannot deliver the validated workload without saturation.