QA How-To
Performance testing with k6 scripts: Step by Step (2026)
Master performance testing with k6 scripts using runnable APIs, workload models, thresholds, correlation, CI gates, metrics, and practical analysis safely.
23 min read | 3,256 words
TL;DR
A useful k6 test is an executable performance hypothesis. Model a real journey, control the offered workload, validate responses with checks, enforce SLO-derived thresholds, and correlate k6 metrics with server telemetry before making a release decision.
Key Takeaways
- Start every k6 script with a test objective, workload model, environment boundary, and measurable pass criteria.
- Use checks to classify valid responses and thresholds to make service-level expectations affect the process exit code.
- Choose closed-model VU executors for concurrent-user behavior and arrival-rate executors for externally driven request flow.
- Tag requests by business operation so a fast endpoint cannot hide a slow critical transaction.
- Correlate dynamic IDs and tokens inside each virtual user journey instead of hard-coding shared state.
- Run small deterministic tests in CI, then schedule larger load, stress, and soak tests in controlled environments.
- Interpret latency, errors, throughput, dropped iterations, and application telemetry together.
Performance testing with k6 scripts turns a workload hypothesis into executable JavaScript or TypeScript. A strong script does more than send many requests: it models a defined user journey, applies a controlled load, verifies response validity, and fails when agreed performance criteria are not met.
This guide starts with a local API that you own, then builds smoke, load, stress, and CI workflows using the current k6 v2 JavaScript APIs. The examples are intentionally small enough to run, but the reasoning scales to production services.
TL;DR
| Script concern | k6 mechanism | Why it matters |
|---|---|---|
| User behavior | Exported scenario functions | Keeps business journeys explicit |
| Workload shape | scenarios and executors |
Controls concurrency or arrival rate |
| Functional validity | check() |
Separates valid from invalid responses |
| Release criteria | thresholds |
Produces a failing exit code on breach |
| Business breakdown | Request and metric tags | Prevents aggregate averages from hiding pain |
| Unique test data | SharedArray, setup data, correlation |
Avoids unrealistic collisions |
| Diagnosis | Built-in metrics plus service telemetry | Connects symptoms to causes |
Never load test a public endpoint, vendor sandbox, or production system without explicit authorization and an agreed traffic window. The examples below target a disposable local service.
1. Define the goal for performance testing with k6 scripts
Write a one-sentence objective before code. A useful example is: verify that the order API sustains 20 completed buyer journeys per second for 10 minutes, with fewer than 1 percent failed requests and p95 order latency below the team-approved target. The exact numbers must come from product usage, service objectives, or an intentional experiment, not from a tutorial.
Clarify the system boundary. Are you measuring only an API, the database behind it, a service chain, or the browser experience? Protocol-level k6 HTTP tests are excellent for backend throughput and latency. They do not reproduce browser rendering, layout, or JavaScript main-thread work. Use a browser scenario when those frontend signals are the objective, and do not treat protocol response time as page experience.
Document four inputs:
- The business journey and operation mix.
- The workload source, such as concurrent sessions or arrivals per second.
- The environment, data volume, dependencies, and test window.
- The pass criteria and the evidence required for diagnosis.
Separate questions that need different tests. A load test checks expected traffic. A stress test explores behavior above expectation and recovery. A spike test examines sudden change. A soak test looks for degradation over time. A smoke performance test proves the script and environment work at minimal load. Combining all objectives into one ramp makes the result ambiguous.
If you need a more introductory tour of installation and basic syntax, start with the k6 load testing tutorial. Here, the emphasis is on designing scripts that support defensible decisions.
2. Install k6 and create a safe local target
Install k6 using the package instructions for your operating system, or use the official container image. Confirm the executable and major version:
k6 version
node --version
Create server.mjs with a tiny dependency-free Node.js API. It exposes health, products, and order creation so the script can exercise reads, JSON parsing, validation, and correlation:
import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
const products = [
{ id: 'p-100', name: 'Keyboard', price: 70 },
{ id: 'p-200', name: 'Mouse', price: 35 }
];
const server = createServer((request, response) => {
response.setHeader('content-type', 'application/json');
if (request.method === 'GET' && request.url === '/health') {
response.writeHead(200);
response.end(JSON.stringify({ status: 'UP' }));
return;
}
if (request.method === 'GET' && request.url === '/products') {
response.writeHead(200);
response.end(JSON.stringify(products));
return;
}
if (request.method === 'POST' && request.url === '/orders') {
let body = '';
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
const order = JSON.parse(body);
response.writeHead(201);
response.end(JSON.stringify({
id: randomUUID(),
productId: order.productId,
quantity: order.quantity,
status: 'ACCEPTED'
}));
});
return;
}
response.writeHead(404);
response.end(JSON.stringify({ error: 'not found' }));
});
server.listen(3000, '127.0.0.1', () => {
console.log('Local target listening on http://127.0.0.1:3000');
});
Run it in one terminal:
node server.mjs
Verify it in another:
curl -i http://127.0.0.1:3000/health
This service is for learning, not benchmarking Node.js. It has no persistence and minimal error handling. Its purpose is to make every following command reproducible without directing traffic at a system you do not own.
3. Write the first runnable k6 performance test
Create scripts/smoke.js. k6 does not run on Node.js, even though the script language is JavaScript. Import APIs from k6 modules, not arbitrary Node built-ins.
import http from 'k6/http';
import { check } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://127.0.0.1:3000';
export const options = {
vus: 1,
iterations: 1,
thresholds: {
checks: ['rate==1'],
http_req_failed: ['rate==0']
}
};
export default function () {
const response = http.get(BASE_URL + '/products', {
tags: { endpoint: 'list_products' }
});
check(response, {
'products status is 200': (r) => r.status === 200,
'products array is not empty': (r) => {
const body = r.json();
return Array.isArray(body) && body.length > 0;
}
});
}
Run it:
k6 run -e BASE_URL=http://127.0.0.1:3000 scripts/smoke.js
The check() call records boolean outcomes. A failed check alone does not necessarily fail the k6 process. The checks threshold converts any failed check in this tiny smoke run into a failed test. The http_req_failed threshold separately catches network failures and unexpected HTTP responses according to k6's expected-response logic.
Keep the first run at one iteration. You are validating URL, data parsing, imports, credentials, and checks, not measuring capacity. If this fails, increasing virtual users only multiplies noise.
Read the console summary. Confirm that one iteration completed, the checks passed, and no requests failed. Also inspect the target log and response manually. A technically green script with the wrong endpoint or a weak check is worse than no performance test because it creates false confidence.
4. Model a complete business journey and correlate data
Real performance behavior comes from journeys, not isolated endpoints. A buyer lists products, chooses one, creates an order, and receives a new order ID. The script must extract the product ID from the first response and send it in the next request. That is correlation.
Create scripts/order-journey.js:
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const BASE_URL = __ENV.BASE_URL || 'http://127.0.0.1:3000';
const businessFailures = new Rate('business_failures');
const orderDuration = new Trend('order_duration', true);
export const options = {
vus: 3,
duration: '20s',
thresholds: {
http_req_failed: ['rate<0.01'],
checks: ['rate>0.99'],
business_failures: ['rate<0.01'],
'order_duration': ['p(95)<500']
}
};
export default function () {
let productId;
group('browse products', () => {
const response = http.get(BASE_URL + '/products', {
tags: { endpoint: 'list_products' }
});
const valid = check(response, {
'product list returned': (r) => r.status === 200,
'product list has data': (r) => Array.isArray(r.json()) && r.json().length > 0
});
businessFailures.add(!valid);
if (valid) {
productId = response.json()[0].id;
}
});
if (!productId) {
return;
}
group('create order', () => {
const response = http.post(
BASE_URL + '/orders',
JSON.stringify({ productId: productId, quantity: 1 }),
{
headers: { 'content-type': 'application/json' },
tags: { endpoint: 'create_order' }
}
);
orderDuration.add(response.timings.duration);
const valid = check(response, {
'order created': (r) => r.status === 201,
'order id returned': (r) => Boolean(r.json('id')),
'order accepted': (r) => r.json('status') === 'ACCEPTED'
});
businessFailures.add(!valid);
});
sleep(1);
}
Each virtual user repeats the journey independently. The product ID is local to one iteration, and the server returns a unique order ID. In a production script, correlation may include authentication tokens, CSRF values, cart IDs, polling links, and cleanup identifiers. Never hard-code a token captured from one browser session and share it across all virtual users.
5. Choose closed and open workload models correctly
k6 executors define how iterations are scheduled. The most important decision is whether the workload is closed or open.
In a closed model, a fixed set of virtual users starts a new iteration after completing the previous one, including any deliberate think time. If the system slows, each user completes fewer journeys. This is appropriate for session-like behavior where a user waits for a response.
In an open model, new iterations are scheduled at an arrival rate independently of prior response time, as long as enough VUs are available. This is appropriate when external demand continues to arrive even while the service slows.
| Question | Useful executor | Model |
|---|---|---|
| Can 20 concurrent users complete the journey? | constant-vus |
Closed |
| What happens as concurrent users ramp up? | ramping-vus |
Closed |
| Can the service sustain 50 arrivals per second? | constant-arrival-rate |
Open |
| Where does capacity fail across rising traffic? | ramping-arrival-rate |
Open |
| Can a fixed batch finish within a deadline? | shared-iterations |
Closed work pool |
| Must every VU execute an exact count? | per-vu-iterations |
Closed |
A constant-arrival-rate configuration looks like this:
export const options = {
scenarios: {
orders: {
executor: 'constant-arrival-rate',
exec: 'orderJourney',
rate: 20,
timeUnit: '1s',
duration: '2m',
preAllocatedVUs: 30,
maxVUs: 80
}
}
};
export function orderJourney() {
// Call the same correlated request flow used in the earlier example.
}
Do not add sleep() merely to pace an arrival-rate executor. The executor already spaces iteration starts. In contrast, think time is part of a closed user model because it represents user behavior.
Watch dropped_iterations. If k6 lacks available VUs to start the requested open-model iterations, the offered workload was not fully delivered. That can indicate insufficient VU allocation or severe target slowdown. A latency chart without this context can be misleading.
6. Turn requirements into k6 checks and thresholds
Checks ask whether individual responses are valid. Thresholds ask whether aggregate metrics met acceptance criteria. Use both. A fast 500 response should not improve a performance result, and a valid but consistently slow response should not pass a release gate.
Built-in metrics include http_req_duration, http_req_failed, http_reqs, data volume, iteration duration, VUs, and dropped iterations. Tag-specific thresholds isolate business operations:
export const options = {
thresholds: {
http_req_failed: ['rate<0.01'],
'http_req_duration{endpoint:list_products}': ['p(95)<250'],
'http_req_duration{endpoint:create_order}': [
{ threshold: 'p(95)<500', abortOnFail: false }
],
checks: ['rate>0.99'],
dropped_iterations: ['count==0']
}
};
Treat the values as placeholders until stakeholders approve them. Derive latency criteria from service-level objectives or user experience budgets. Define whether the percentile applies to the full test, a steady-state interval, a scenario, or a tagged endpoint. Global p95 can hide a rare but important operation.
Use long threshold form with abortOnFail only when continuing is wasteful or unsafe. Early abort can protect an unstable environment, but it also shortens the evidence window. For a diagnostic stress test, continuing through the planned stages may be the objective.
Be precise about failure rates. http_req_failed and a business failure metric answer different questions. An HTTP 200 response with an empty or semantically invalid body may not count as an HTTP failure, but it should fail a check and increment the custom business-failure rate.
The API error handling and negative testing guide helps define response validity beyond status codes. Those functional oracles make performance results credible.
7. Use test data without creating artificial contention
Test data can distort the system. If every VU updates one customer or buys the same final inventory item, lock contention and validation errors may dominate. That may be useful for a targeted contention experiment, but it is not automatically representative traffic.
Use SharedArray for a large read-only array loaded once in the init context:
import { SharedArray } from 'k6/data';
import exec from 'k6/execution';
const users = new SharedArray('users', () => {
return JSON.parse(open('./users.json'));
});
export default function () {
const index = exec.vu.idInTest % users.length;
const user = users[index];
// Authenticate or perform the journey with this user's credentials.
}
The callback must return an array. Keep the SharedArray in init context and read individual elements. It is not a shared mutable store and should not be used for communication between VUs.
Choose allocation based on semantics. Modulo assignment reuses records when VUs exceed data length. That is acceptable only if the user can support concurrent sessions. For single-session accounts, validate that the dataset is at least as large as the maximum active VUs and fail setup otherwise.
Generate unique business keys from a test run identifier, scenario, VU ID, and iteration when the target accepts them. Avoid high-cardinality metric tags such as a unique order ID. Tags create metric series, so use stable dimensions such as operation, region, outcome class, or scenario.
Sanitize production-derived data and never package real credentials in the repository. Secret values belong in environment injection or an approved secret source. Logs and failed-response bodies must also avoid leaking them.
8. Add setup, teardown, and environment safety controls
k6 has lifecycle stages. Init-context code defines options and loads files. setup() runs once and can prepare shared data. Scenario functions run many times across VUs. teardown(data) runs once after execution and can clean up using data returned from setup.
Use setup for a health check or run-level resource, not for creating every user's mutable state:
import http from 'k6/http';
import exec from 'k6/execution';
const BASE_URL = __ENV.BASE_URL || 'http://127.0.0.1:3000';
export function setup() {
const response = http.get(BASE_URL + '/health');
if (response.status !== 200) {
exec.test.abort('Target health check failed');
}
return { runId: __ENV.RUN_ID || 'local' };
}
export default function (data) {
http.get(BASE_URL + '/products', {
tags: { run_type: data.runId }
});
}
export function teardown(data) {
console.log('Completed performance run ' + data.runId);
}
An abort is appropriate when the test cannot answer its objective, such as an unhealthy deployment or invalid credentials. Do not interpret an aborted run as a passed performance test.
Add safety at multiple layers: an allowlist of permitted hosts, a maximum duration, a maximum VU or arrival-rate value, an environment banner, and explicit approval for high-load stages. CI variables should distinguish local, test, staging, and production targets. Avoid a default production URL.
Teardown is best-effort, not a guaranteed transaction. A killed process or lost runner may not execute it. Use resource expiry, run-specific namespaces, and an independent janitor for durable test data. For very large cleanup volumes, calling one delete-by-run-ID endpoint is safer than thousands of teardown requests.
9. Build a test suite instead of one giant script
Create separate scripts or configurations for different questions. A practical repository might contain:
performance/
data/
users.json
lib/
client.js
checks.js
scenarios/
smoke.js
load.js
stress.js
soak.js
README.md
The smoke script uses minimal load and strict functional checks. Run it on pull requests against a disposable environment. The load script represents expected demand and can run on a stable shared environment after deployment. Stress and soak tests should be manually approved or scheduled because they consume more capacity and require active observation.
Share request helpers and check functions, but keep workload options close to the scenario that owns them. Excess abstraction makes performance scripts hard to review. A reader should be able to identify the offered load, journey, and thresholds without tracing a framework of wrappers.
Version scripts with the application. Review changes to thresholds as requirements changes, not casual refactoring. Require a reason when a limit is relaxed. Store an environment manifest with application commit, infrastructure configuration, dataset scale, k6 version, and relevant feature flags for significant runs.
Do not compare results across incompatible environments. A test against a small ephemeral database and one against a production-sized staging database answer different questions. Baselines are meaningful only when workload and environment are controlled.
10. Run k6 in CI without creating a fragile gate
CI is ideal for short, deterministic checks. It is often a poor place for a large load test because shared runners, queueing, autoscaling, and noisy test environments add variation. Use layered automation.
A simple GitHub Actions job can run k6 through its official container against a service started in the same job:
name: k6 smoke performance
on:
pull_request:
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: lts/*
- name: Start local target
run: |
node server.mjs > server.log 2>&1 &
echo $! > server.pid
- name: Wait for health
run: |
for attempt in {1..30}; do
curl --fail http://127.0.0.1:3000/health && exit 0
sleep 1
done
exit 1
- name: Run k6 smoke test
run: |
docker run --rm --network host -v "$PWD:/work" -w /work grafana/k6:latest run scripts/smoke.js
For reproducibility, pin the container to a reviewed k6 release in your actual repository and update it intentionally. The moving tag keeps this tutorial from fabricating a patch version, but it is not a long-term dependency policy.
A threshold breach makes k6 run exit nonzero, which fails the job. Preserve logs and exported outputs on failure if your workflow produces them. Keep the CI smoke duration short enough for repeatable feedback. Schedule larger tests on controlled load generators and alert service owners before execution.
11. Interpret k6 and server metrics as one experiment
Start with validity. Did the intended arrival rate occur? Were there dropped iterations? Did checks pass? Were HTTP failures and business failures within criteria? Only then interpret latency.
Read percentiles by tagged operation. Average latency can remain attractive while a minority of users wait too long. Compare p50, p90, p95, and p99 only when sample size and test duration support them. A very short run may not provide a stable tail.
Throughput is an outcome and an input, depending on the model. With a closed model, target slowdown reduces completed iterations, so rising latency and falling throughput occur together. With an arrival-rate model, requested arrivals are an input, while dropped iterations show k6 could not start them all.
Correlate the exact test window with application telemetry:
- Request rate, status, and duration at ingress.
- CPU throttling, memory, garbage collection, and event-loop or thread-pool saturation.
- Database connections, query latency, locks, and cache hit ratio.
- Queue depth, consumer lag, retries, and dependency latency.
- Deployment events, autoscaling changes, and error logs.
Do not claim causation from k6 alone. High latency plus database lock time suggests a hypothesis that logs or traces should confirm. Client-side saturation is another possibility, so monitor the load generator's CPU, memory, network, and open files.
Rate-limit behavior deserves its own experiment. Read API rate limiting testing to distinguish a correct 429 protection response from an unintended capacity failure.
12. Scale performance testing with k6 scripts responsibly
Move from local to shared environments in steps. First prove script correctness with one iteration. Next run a short smoke test. Then validate expected load for a limited duration. Only after monitoring and safety controls are ready should you run stress, spike, soak, distributed, or production experiments.
Distributed execution introduces result aggregation, clock alignment, network geography, data partitioning, and load-generator capacity. Confirm that a single generator is actually saturated before distributing. Otherwise additional nodes add operational complexity without improving the experiment.
Maintain a performance test catalog. For each script, record owner, purpose, environment, workload, threshold source, data requirements, expected duration, approval level, and dashboard link. Retire scripts that no longer represent product behavior. An obsolete journey can pass while real users suffer.
Review results as evidence, not a contest. A regression may come from application code, infrastructure, data growth, downstream services, or the test harness. Preserve the run manifest and change one factor at a time. When an optimization improves latency, verify error rate, resource use, and correctness rather than assuming the trade is free.
Performance testing with k6 scripts becomes valuable when teams can repeat the same question and trust the comparison. Script quality, environment control, and diagnosis discipline matter more than a spectacular virtual-user count.
Interview Questions and Answers
Q: What is the difference between a k6 check and a threshold?
A check evaluates a boolean condition for individual script activity and records a rate. A threshold evaluates aggregate metric data against a criterion and can make the test process exit unsuccessfully. Use checks for response validity and thresholds for release decisions.
Q: When would you use constant-arrival-rate instead of constant-vus?
Use constant-arrival-rate when external demand should continue at a defined rate regardless of response time. Use constant-vus when modeling a fixed number of users who wait for each journey to complete. The first is open model, while the second is closed model.
Q: What do dropped iterations mean?
They mean k6 could not start scheduled iterations, commonly because an arrival-rate scenario had too few available VUs as iteration duration increased. The intended load was not fully delivered. Increase allocation only after checking whether target slowdown is the real signal.
Q: Why are HTTP 200 responses not enough for a performance test?
A response can be fast and return 200 while containing an empty result, error object, or wrong business state. Validate status, schema, and key business values. Track semantic failures separately so invalid fast responses do not improve the performance result.
Q: How do you prevent test data from distorting results?
Match data reuse to production behavior, allocate isolated users when sessions are exclusive, and generate run-specific keys for mutable entities. Avoid one shared hot record unless contention is the explicit objective. Keep metric tags low-cardinality.
Q: What should run in pull request CI?
A small smoke performance test that validates the script, critical journey, and a few robust regression thresholds can run in CI. Larger load, stress, and soak tests usually belong in controlled scheduled or approved workflows with stable infrastructure and monitoring.
Q: How do you investigate a p95 latency regression?
First verify checks, delivered workload, dropped iterations, and load-generator health. Break latency down by tagged operation and align the test window with traces, database metrics, queues, dependencies, and deployment events. Form a hypothesis, then rerun a controlled comparison.
Q: Why should you avoid sleep in an arrival-rate scenario?
Arrival-rate executors already schedule iteration starts at the configured rate. Adding sleep as pacing changes iteration duration and VU requirements without controlling arrival timing. Include delays only when they are meaningful behavior inside the journey.
Common Mistakes
- Starting with a large VU count: Prove one correct iteration and a minimal smoke test before increasing load.
- Testing a system without authorization: Obtain ownership, environment, window, capacity, and emergency-stop approval.
- Using checks without thresholds: Failed checks may be visible while the process still succeeds, so connect critical rates to thresholds.
- Using only global latency: Tag critical operations and enforce criteria per journey or endpoint.
- Confusing concurrent users with requests per second: Closed and open models respond differently when the target slows.
- Ignoring dropped iterations: The test may not have delivered the configured arrival rate.
- Sharing one mutable account: Session invalidation and data locks produce artificial failures.
- Adding unique IDs as metric tags: High-cardinality series can overwhelm result storage and analysis.
- Treating the load generator as infinite: Monitor its CPU, memory, network, ports, and VU allocation.
- Changing workload and application together: Preserve manifests and vary one factor when comparing performance.
- Running a large load test on every commit: Use a layered cadence so CI remains stable and high-load tests remain controlled.
- Relaxing a threshold to restore green status: Require evidence that the service objective changed, not just the latest result.
Conclusion
Performance testing with k6 scripts is a disciplined loop: state the question, model the workload, write a valid journey, enforce thresholds, observe both client and server, and preserve enough context to reproduce the result. The code is compact, but trustworthy conclusions require careful boundaries and data.
Start the local API, run the one-iteration smoke script, and inspect every check and metric. Then add one realistic scenario at a time. That incremental approach produces a performance suite the delivery team can safely automate and use for decisions.
Interview Questions and Answers
What are the main parts of a k6 test script?
The init context defines imports, options, and read-only data. Optional setup prepares run-level state, scenario functions generate workload across VUs, and teardown performs best-effort cleanup. Checks validate responses, while thresholds evaluate aggregate metrics.
How do checks and thresholds work together?
Checks label individual outcomes as true or false and feed the checks rate metric. A threshold such as checks rate greater than 0.99 makes that aggregate expectation a pass or fail condition. This separates validation logic from acceptance policy.
Explain open and closed workload models.
A closed model fixes concurrent users, and each user waits before starting its next iteration. An open model schedules new arrivals at an external rate, independent of prior completion while VUs are available. They answer different capacity questions when latency rises.
What causes dropped iterations in k6?
Arrival-rate executors drop a scheduled iteration when no VU is available to start it. Common causes are insufficient preallocation or longer iterations due to target slowdown. The result means the requested arrival workload was not fully generated.
How would you correlate dynamic data in k6?
Extract the token or entity ID from one response, validate it, and pass it into the next request within the same VU journey. Keep mutable correlated state local to the iteration or VU. Avoid sharing one captured value across all users unless production behaves that way.
When would you create a custom Rate metric?
I use a custom Rate for a semantic outcome not represented by HTTP transport metrics, such as rejected orders or invalid business state. I add true for failure, false for success, and define a threshold on the rate.
Why should endpoint requests have tags?
Tags let results and thresholds be filtered by stable business operation. Without them, a high-volume fast endpoint can hide a low-volume slow transaction in global latency. Tags should stay low-cardinality.
What is SharedArray used for?
SharedArray loads an array once in init context and shares its serialized backing data across VUs, reducing repeated memory use for larger datasets. It is read-only and is not a communication mechanism between virtual users.
How do you determine a k6 workload?
I use production traffic shape, concurrency, arrival rates, operation mix, think time, and business forecasts. I document assumptions and separate expected load from stress exploration. Tutorial numbers are never substituted for system requirements.
How do you know whether the load generator is the bottleneck?
Monitor generator CPU, memory, network, open files, VUs, and dropped iterations. Compare client and server request counts and consider a controlled distributed run. A saturated generator invalidates claims about target capacity.
What would you automate in CI?
I automate a short deterministic smoke or regression workload with robust thresholds and a disposable or stable target. Larger load, stress, spike, and soak tests run on approved schedules or manual workflows with monitoring and ownership.
How do you analyze a failed performance threshold?
I first confirm functional validity and delivered load, then isolate the affected tagged operation and time window. I correlate k6 metrics with service logs, traces, resource saturation, databases, queues, and dependencies. I preserve the environment and workload manifest before rerunning a controlled comparison.
Frequently Asked Questions
Is k6 suitable for API performance testing?
Yes. k6 provides HTTP APIs, scenarios, checks, thresholds, custom metrics, tags, and output integrations for protocol-level API performance testing. It can model correlated multi-request journeys rather than only isolated endpoints.
Do failed k6 checks fail the test?
A check records pass or fail outcomes but does not by itself guarantee a nonzero process exit. Add a threshold on checks or a related custom metric when a failure must affect the test result.
What is the difference between VUs and iterations in k6?
A VU is a virtual user that executes scenario code. An iteration is one execution of that scenario function, and a VU may complete many iterations depending on the executor, duration, and response time.
How do I set p95 response time in a k6 script?
Add a threshold such as http_req_duration: [p(95)<500] using valid string syntax in the options object. Prefer a tagged metric such as http_req_duration{endpoint:create_order} when different operations have different objectives.
Should k6 tests use production data?
Use representative scale and shape, but sanitize any copied data and never store real credentials or personal information in scripts. Generate isolated test entities or use approved synthetic datasets.
Can k6 performance tests run in GitHub Actions?
Yes. Short smoke or regression scripts can run as CLI or container steps, and threshold breaches fail the job through the k6 exit code. Larger tests need controlled runners, a stable environment, and monitoring.
How long should a k6 load test run?
Duration depends on the objective. A script smoke check may take seconds, a steady load test needs enough time for representative behavior, and a soak test may run much longer to expose resource drift. Document why the chosen window is sufficient.