QA How-To
K6 load testing tutorial: Step by Step (2026)
Follow this k6 load testing tutorial to install k6 v2, script realistic API traffic, set thresholds, analyze metrics, and automate CI safely for teams in 2026.
27 min read | 3,856 words
TL;DR
Install k6 v2, validate one request with a smoke test, model the real workload with the correct scenario executor, add functional checks and measurable thresholds, then run only against an authorized environment. A useful k6 result combines delivered load, latency percentiles, failure rate, business correctness, and server-side evidence.
Key Takeaways
- Start with a measurable business question and an authorized target, not an arbitrary virtual-user count.
- Use a smoke test to prove requests, checks, test data, tags, and observability before increasing load.
- Choose a VU executor for closed user behavior and an arrival-rate executor for independently arriving work.
- Combine checks with thresholds because a failed check alone does not make the k6 process fail.
- Segment latency by stable operation tags and interpret percentiles with throughput, errors, and sample count.
- Monitor the load generator and dropped iterations so configured demand is not mistaken for delivered demand.
- Keep CI workloads short and controlled, while reserving stress, spike, and soak tests for coordinated environments.
A practical k6 load testing tutorial should take you from an empty folder to a test you can defend, not merely a script that produces traffic. This guide shows how to install current k6 v2, run a safe local target, write realistic HTTP journeys, select an executor, enforce thresholds, analyze results, and add a small CI test.
The goal is not to discover the largest virtual-user number your laptop can display. The goal is to answer a defined reliability question with controlled demand and trustworthy evidence. Run meaningful load only against systems you own or targets for which the owner has explicitly approved the workload, timing, and abort plan.
Examples use JavaScript supported by the k6 runtime. k6 scripts look familiar to Node.js developers, but k6 is not Node.js, so Node built-ins and arbitrary npm packages are not automatically available inside a test.
TL;DR
| Step | Action | Proof before moving on |
|---|---|---|
| 1 | Define the objective and authorized target | Workload, criteria, owners, and abort conditions are written |
| 2 | Install k6 v2 and start a local API | k6 version and the health endpoint succeed |
| 3 | Run one smoke iteration | Status, payload, checks, and tags are correct |
| 4 | Select a scenario executor | The executor matches closed users or open arrivals |
| 5 | Add thresholds | Criteria are measurable and k6 returns failure when violated |
| 6 | Run the planned profile | Configured load was actually delivered |
| 7 | Correlate evidence | Client results align with service metrics, logs, and traces |
| 8 | Automate a small test | CI failures are reproducible and owned |
The short rule is: objective -> smoke -> model -> execute -> validate -> diagnose. Never skip validation and jump directly from a p95 number to a capacity claim.
1. k6 load testing tutorial roadmap and test objective
k6 is an open-source reliability and performance testing tool whose test logic is written in JavaScript or TypeScript. It can drive protocol-level HTTP traffic efficiently, supports scenarios for different workload models, exposes built-in and custom metrics, and turns performance criteria into thresholds. The official first-test documentation defines the same core structure used here: imports, options, a default function, and optional lifecycle functions.
Before writing code, convert the request into a testable question. Test 500 users is incomplete. A better objective is: Can release candidate 42 complete the catalog and order journey at the forecast peak operation mix for 20 minutes while meeting the agreed latency, business-success, and error objectives in the staging performance environment? The numbers still need evidence, but the statement identifies a build, workflow, duration, criteria, and environment.
Create a one-page test charter with:
- Target host, build, infrastructure shape, and system owner.
- Business operations and their traffic mix.
- Whether demand represents active sessions or independently arriving work.
- Test accounts, payload distribution, cache state, and cleanup approach.
- Latency, error, throughput, and business-correctness criteria.
- Server dashboards, logs, traces, and generator monitoring.
- Safe maximum load, abort thresholds, and contacts.
Do not run this tutorial as a stress test against Grafana's demo application or any unrelated public website. The local API in the next section exists so you can learn safely. For broader role preparation, see the performance engineering career guide.
2. Install k6 v2 and create the tutorial workspace
In 2026, the current documentation is on k6 v2. The familiar protocol APIs such as http.get(), check(), scenarios, and thresholds remain available, but older articles can contain removed CLI features. The k6 v2 migration guide records important changes, including removal of k6 pause, k6 resume, k6 scale, k6 status, and the externally-controlled executor. It also replaces the old --no-summary flag with --summary-mode=disabled.
Install with Homebrew on macOS:
brew install k6
k6 version
Or pull the official container image:
docker pull grafana/k6
docker run --rm grafana/k6 version
Create a small workspace:
mkdir k6-tutorial
cd k6-tutorial
mkdir scripts data reports
A useful repository layout is scripts/ for executable tests, data/ for approved synthetic input, and reports/ for ignored local output. Keep reusable configuration in modules only when repetition appears. A first script with five abstraction layers is harder to validate than one readable journey.
Native installation is easiest when the target runs on localhost. A containerized k6 process has its own network namespace, so localhost inside the container is not the host machine. On Docker Desktop, a script can usually target http://host.docker.internal:3000. Network setup differs on Linux and in CI, so verify connectivity with one iteration before blaming the application.
Commit scripts and nonsecret synthetic data. Do not commit passwords, tokens, customer records, cloud credentials, or an exported report containing sensitive URLs. Pass environment-specific values at execution time and use the platform's secret store for credentials.
3. Start a safe local API for runnable k6 examples
The following tiny server uses only the Node.js standard library. It provides health, login, product, and order endpoints so every k6 example is runnable without sending load to the internet. Save it as server.mjs in the tutorial workspace. The artificial delays are illustrative, not benchmark expectations.
import { createServer } from 'node:http';
const products = [
{ id: 'p-100', name: 'Keyboard', price: 49 },
{ id: 'p-200', name: 'Mouse', price: 29 },
];
function send(response, status, body) {
response.writeHead(status, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(body));
}
const server = createServer((request, response) => {
const url = new URL(request.url, 'http://localhost:3000');
if (request.method === 'GET' && url.pathname === '/health') {
return send(response, 200, { status: 'ok' });
}
if (request.method === 'POST' && url.pathname === '/login') {
return send(response, 200, { token: 'local-demo-token' });
}
if (request.method === 'GET' && url.pathname === '/products') {
return setTimeout(() => send(response, 200, products), 25);
}
if (request.method === 'POST' && url.pathname === '/orders') {
if (request.headers.authorization !== 'Bearer local-demo-token') {
return send(response, 401, { error: 'unauthorized' });
}
return setTimeout(
() => send(response, 201, { id: `o-${Date.now()}`, status: 'accepted' }),
40,
);
}
return send(response, 404, { error: 'not found' });
});
server.listen(3000, () => {
console.log('Local tutorial API listening on http://localhost:3000');
});
Start it in terminal one:
node server.mjs
Verify it in terminal two:
curl -s http://localhost:3000/health
You should receive {'status':'ok'} in JSON form, with standard double quotes in the actual response. Keep this deliberately small server for learning syntax only. It has no database, production network, queues, cache, rate limits, or realistic resource constraints, so its results cannot support a production capacity conclusion. That limitation is a useful lesson: k6 can measure the target you give it, but it cannot make the environment representative.
4. Write and run the first k6 smoke test
Start with one virtual user and one iteration. This is a script validation test, not load. Save the following as scripts/smoke.js. It uses current k6 imports, the __ENV object for an overridable base URL, a named request, functional checks, and thresholds.
import http from 'k6/http';
import { check } from 'k6';
const baseUrl = __ENV.BASE_URL || 'http://localhost:3000';
export const options = {
vus: 1,
iterations: 1,
thresholds: {
checks: ['rate==1'],
http_req_failed: ['rate==0'],
'http_req_duration{name:health}': ['p(95)<250'],
},
};
export default function () {
const response = http.get(`${baseUrl}/health`, {
tags: { name: 'health' },
timeout: '3s',
});
check(response, {
'health status is 200': (res) => res.status === 200,
'health body reports ok': (res) => res.json('status') === 'ok',
});
}
Run it:
k6 run scripts/smoke.js
For a different host, pass a script environment variable explicitly:
k6 run -e BASE_URL=https://authorized-test.example scripts/smoke.js
The -e flag makes BASE_URL available through __ENV; it does not apply k6 option variables magically. For example, -e K6_VUS=10 merely gives the script a value unless your code reads it, while a shell environment variable such as K6_VUS=10 k6 run ... configures the matching k6 option. Keep these mechanisms distinct.
Inspect the check names and threshold marks in the end-of-test summary. Temporarily change /health to /missing and confirm the test fails, then restore it. This negative control proves your gate detects the defect it claims to detect.
5. Build a realistic API journey with lifecycle functions
A k6 script moves through init context, optional setup(), VU execution, and optional teardown(). Imports, options, file loading, and metric declarations belong in init context. setup() runs once and can make HTTP calls. The default or named scenario function runs once per iteration. teardown() runs once after VU work when setup completed normally.
Save this fuller test as scripts/order-load.js:
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const baseUrl = __ENV.BASE_URL || 'http://localhost:3000';
const businessFailures = new Rate('business_failures');
export const options = {
scenarios: {
shoppers: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '20s', target: 5 },
{ duration: '40s', target: 5 },
{ duration: '20s', target: 0 },
],
gracefulRampDown: '10s',
},
},
thresholds: {
checks: ['rate>0.99'],
business_failures: ['rate<0.01'],
http_req_failed: ['rate<0.01'],
'http_req_duration{name:list_products}': ['p(95)<300'],
'http_req_duration{name:create_order}': ['p(95)<500'],
},
};
export function setup() {
const response = http.post(`${baseUrl}/login`, JSON.stringify({
username: 'load-user',
password: 'local-only',
}), {
headers: { 'Content-Type': 'application/json' },
tags: { name: 'login_setup' },
});
const valid = check(response, {
'setup login succeeds': (res) => res.status === 200,
'setup token exists': (res) => Boolean(res.json('token')),
});
if (!valid) {
throw new Error('Setup login failed');
}
return { token: response.json('token') };
}
export default function (data) {
group('browse and order', () => {
const productsResponse = http.get(`${baseUrl}/products`, {
tags: { name: 'list_products' },
timeout: '3s',
});
const productsOk = check(productsResponse, {
'products status is 200': (res) => res.status === 200,
'products are returned': (res) => Array.isArray(res.json()) && res.json().length > 0,
});
if (!productsOk) {
businessFailures.add(true);
return;
}
const productId = productsResponse.json()[0].id;
const orderResponse = http.post(`${baseUrl}/orders`, JSON.stringify({
productId,
quantity: 1,
}), {
headers: {
Authorization: `Bearer ${data.token}`,
'Content-Type': 'application/json',
},
tags: { name: 'create_order' },
timeout: '3s',
});
const orderOk = check(orderResponse, {
'order status is 201': (res) => res.status === 201,
'order is accepted': (res) => res.json('status') === 'accepted',
});
businessFailures.add(!orderOk);
});
sleep(1);
}
Run it only after the smoke test passes. The shared setup token is acceptable for this local teaching API, but it may be unrealistic for a production workflow. If real users authenticate separately, allocate separate approved accounts or tokens and model refresh behavior.
6. Choose k6 scenarios and executors for the workload
Scenarios tell k6 which exported function to run, when to run it, and which executor schedules work. Executor choice changes the workload model, so it is more important than the visual shape of a ramp. The current scenario documentation groups executors by iterations, VUs, and arrival rate.
| Executor | Controls | Workload model | Strong use case | Main validation risk |
|---|---|---|---|---|
shared-iterations |
Total iterations shared by VUs | Finite work | Smoke tests and fixed data processing | Faster VUs may execute more iterations |
per-vu-iterations |
Iterations for each VU | Finite per-user work | Each user must execute the same count | Test duration varies with slow VUs |
constant-vus |
Fixed active VUs | Closed | Stable session population | Throughput falls when iteration time rises |
ramping-vus |
VUs across stages | Closed | Warmup, steady load, and ramp-down | VU count is mistaken for arrival rate |
constant-arrival-rate |
Iteration starts per time unit | Open | Independent requests or events | Insufficient allocated VUs cause dropped iterations |
ramping-arrival-rate |
Arrival rate across stages | Open | Changing external demand | Generator cannot sustain configured arrivals |
Closed models represent a population whose next iteration depends on finishing the current one, plus any pacing. If the system slows, each VU completes less work. Open models schedule iteration starts independently of earlier completion. As response time rises, concurrent work can accumulate, provided the generator has enough VUs and resources.
Neither is universally correct. A signed-in shopping journey can behave like a closed session, while incoming webhooks can behave like open arrivals. One system can require multiple scenarios with different exec functions, tags, and start times.
The vus plus duration shortcuts remain useful for a basic test, and the top-level stages option is a shortcut for one ramping-VUs scenario. Prefer explicit scenarios once workload behavior matters. If your team is comparing tooling before adoption, the JMeter vs k6 guide covers workflow tradeoffs.
7. Model arrival rate, VU allocation, and pacing correctly
Suppose production telemetry shows independently arriving order attempts. A constant-arrival-rate scenario expresses that demand directly. Replace the scenarios block in the prior test with an approved rate and duration:
export const options = {
scenarios: {
order_arrivals: {
executor: 'constant-arrival-rate',
rate: 4,
timeUnit: '1s',
duration: '2m',
preAllocatedVUs: 10,
maxVUs: 30,
exec: 'orderJourney',
},
},
};
export function orderJourney(data) {
// Put the validated browse-and-order journey here.
}
rate: 4 is illustrative. Do not translate a monthly request total into four arrivals per second without accounting for peak windows, business mix, retries, scheduled jobs, geography, and growth. Record the source and observation period for every workload value.
preAllocatedVUs provides workers ready to start iterations. maxVUs allows allocation to grow if iterations take longer, but continuously allocating more VUs can consume generator resources. Watch dropped_iterations. A nonzero value means k6 could not start all scheduled iterations, often because available VUs were busy or the generator was constrained. Configured rate is an intention, not proof of delivery.
Do not add sleep() to pace an arrival-rate executor. The executor schedules starts; sleep merely makes iterations longer and increases required VUs. In a closed VU model, sleep can represent think time or pacing, but the distribution should come from user behavior rather than habit. A fixed one-second pause for every operation is rarely a realistic universal model.
Finally, remember coordinated omission. If a workload model stops generating expected arrivals as the target slows, measured latency can understate what users under continuing demand would experience. Arrival-rate executors address one common source, but generator saturation, client timeouts, dropped work, and an incorrectly chosen measurement boundary can still invalidate the result.
8. Use checks, thresholds, tags, and custom metrics
Checks evaluate functional conditions and emit pass or fail measurements. They do not behave like traditional assertions: a failed check continues the iteration and does not by itself produce a failed process exit. The current checks documentation explicitly recommends thresholds when check results must fail a test. That is why the examples use both.
Thresholds are pass or fail criteria over metric aggregates. Trend metrics support values such as avg, med, and p(N). Rate metrics use rate, counters use count or rate, and gauges use value. Set criteria from service objectives or an approved test charter, not from copied tutorial values.
Use stable low-cardinality tags to segment operations:
const response = http.get(`${baseUrl}/products/${productId}`, {
tags: { name: 'get_product' },
});
Without a stable name, dynamic product IDs can create many URL time series. A threshold can target the operation:
export const options = {
thresholds: {
'http_req_duration{name:get_product}': ['p(95)<350'],
'http_req_failed{name:get_product}': ['rate<0.01'],
checks: ['rate>0.99'],
},
};
Use custom metrics only for information that built-ins and tags cannot express cleanly. Rate suits business failure, Trend captures distributions such as end-to-end workflow time, Counter counts events, and Gauge records the latest or extreme value. Avoid a custom metric per user, order, or unique URL. High-cardinality tags inflate time series, memory use, storage, and dashboard complexity.
For an early safety stop, the long threshold form supports abortOnFail: true and delayAbortEval. Delay evaluation long enough to collect meaningful data and coordinate the abort behavior with system owners. An immediate abort based on one warmup sample can be as misleading as no abort at all.
9. Parameterize data and correlate dynamic responses
Real tests vary accounts, search terms, products, payload sizes, and entity state without creating random nonsense. Parameterization supplies those inputs. Correlation captures a value created by one response, such as a token or resource ID, validates it, and sends it into the next request. The order script correlates the login token and first product ID.
For a larger read-only data file, use SharedArray in init context so the underlying array is stored once and elements are copied when accessed. Save approved synthetic users in data/users.json, then load them as follows:
import { SharedArray } from 'k6/data';
import exec from 'k6/execution';
const users = new SharedArray('synthetic users', () =>
JSON.parse(open('../data/users.json')),
);
export default function () {
const index = exec.scenario.iterationInTest % users.length;
const user = users[index];
// Use user.username with an authorized authentication flow.
}
SharedArray must be created in init context and is read-only. Do not return it from setup(), because marshalling the complete array removes its memory advantage. For distributed runs, confirm whether iterationInTest and your allocation strategy provide the uniqueness the business flow requires. Uniqueness, reuse, and contention should be explicit workload properties.
Treat data failures separately from product failures. If the script exhausts accounts, reuses an invalid token, or collides on a supposedly unique order, the run may be invalid even if the service is healthy. Fail setup quickly when prerequisites are missing. During VU execution, record the failed business step and avoid sending dependent requests with empty values.
Protect sensitive data. Prefer synthetic records. If approved masked production-like data is necessary, preserve referential behavior while following retention and access rules. The API security testing basics guide complements this performance workflow, but load authorization does not automatically authorize security probing.
10. Run the test and interpret k6 metrics together
Run the full local profile with a more detailed terminal summary:
k6 run --summary-mode=full scripts/order-load.js
Read the output as a connected set, not a leaderboard of isolated values.
| Metric | What it represents | Question to ask |
|---|---|---|
http_reqs |
HTTP request count and rate | Did operation counts match the intended journey? |
http_req_duration |
Request duration after connection is available through response completion | Which tagged operation and outcome does this distribution cover? |
http_req_failed |
Requests classified as unexpected | Are failures transport, timeout, HTTP, or business errors? |
checks |
Functional check pass rate | Did fast responses contain correct outcomes? |
iterations |
Completed scenario iterations | Does one iteration equal the business unit assumed by the model? |
iteration_duration |
Whole iteration time | Does it include pacing, multiple requests, and client work? |
vus and vus_max |
Active and initialized VUs | Did allocation approach generator limits? |
dropped_iterations |
Scheduled iterations that did not start | Was open-model demand actually delivered? |
data_received and data_sent |
Network payload volume | Did an unexpected payload change affect generator or service behavior? |
A p95 below 300 ms means approximately 95 percent of samples in that selected metric population were at or below the reported boundary. It does not mean every user was fast, the full journey met 300 ms, or the result applies to production. State the operation tag, interval, sample count, error treatment, load, build, and environment.
Validate the run before diagnosing it. Confirm achieved iterations or arrivals, operation mix, checks, data, generator CPU and memory, network capacity, target build, cache state, and telemetry coverage. Then correlate the same time window with service latency, CPU, queues, connection pools, database waits, cache behavior, dependency calls, logs, and traces. A client graph locates symptoms. Cross-layer evidence and a controlled rerun support a bottleneck conclusion.
11. Visualize, export, and report results without losing context
k6 v2 includes a built-in web dashboard. Enable it for a local run:
K6_WEB_DASHBOARD=true k6 run scripts/order-load.js
The dashboard is available at http://127.0.0.1:5665 by default. You can also export a self-contained HTML report at the end:
K6_WEB_DASHBOARD=true \
K6_WEB_DASHBOARD_EXPORT=reports/order-load.html \
k6 run scripts/order-load.js
The web dashboard documentation notes that graphs need a test duration greater than three times the dashboard aggregation period. A ten-second experiment can still validate a script, but it may not create a useful time-series report.
For team observability, stream results to an approved backend and align k6 timestamps with application telemetry. Current k6 supports multiple real-time outputs, including Prometheus remote write and OpenTelemetry. Output configuration, authentication, retention, and metric cardinality deserve review before a large test. Do not expose tokens in a command captured by shell history or CI logs.
A publication-quality result report includes objective, scope, build, environment, workload source, scenarios, data, criteria, run timeline, generator health, threshold result, client metrics, system evidence, findings, limitations, and next decision. Write The order p95 crossed the agreed objective during the 8 arrivals/s hold while database pool wait and queue depth rose rather than The database is slow. The first statement separates evidence from diagnosis. A follow-up experiment, such as changing pool capacity in an otherwise comparable environment, can test the hypothesis.
Keep scripts, dashboards, test identifiers, configuration, and reports traceable to the build. Never compare two pretty screenshots when the workload, environment, data state, or aggregation differs.
12. k6 load testing tutorial workflow for CI and team adoption
CI should begin with a small deterministic workload, not a capacity or soak test on a shared runner. Use an environment that is stable enough for the decision, preserve failure evidence, and assign an owner. A threshold failure should trigger diagnosis, not automatic reruns until one turns green.
This GitHub Actions example installs k6 with Grafana's official setup action and runs the smoke script against an authorized test URL stored as a repository variable:
name: k6 smoke
on:
workflow_dispatch:
pull_request:
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: grafana/setup-k6-action@v1
- name: Run k6 smoke threshold gate
env:
BASE_URL: ${{ vars.PERF_BASE_URL }}
run: |
test -n "$BASE_URL"
k6 run -e BASE_URL="$BASE_URL" scripts/smoke.js
The shell check stops immediately when the variable is absent. If authentication is needed, store the credential as a CI secret and prevent it from appearing in logs or reports. Limit pull-request tests to low-risk demand. Run average-load suites on a controlled schedule or pre-release event, and coordinate stress, spike, soak, or failure injection with environment owners.
When moving old suites to 2026 k6 v2, search for removed externally-controlled scenarios and obsolete CLI commands. Use explicit k6 cloud run for cloud execution, and configure the required Grafana Cloud stack through login, K6_CLOUD_STACK_ID, or the script cloud options. Review extension compatibility because the Go module path for extension maintainers is now go.k6.io/k6/v2.
Team adoption succeeds when workload definitions and criteria have owners. Review scripts like production code, keep a fast smoke profile, version test data rules, and record known environment differences. The performance test engineer interview questions help engineers practice explaining these decisions, not just the syntax.
Interview Questions and Answers
Q: What is the difference between a k6 check and a threshold?
A check evaluates a boolean condition and records pass or fail samples while the iteration continues. A failed check alone does not make k6 exit with failure. A threshold evaluates an aggregate metric condition and can fail the test, so I put a threshold on checks when functional correctness must gate CI.
Q: When would you use ramping-vus instead of ramping-arrival-rate?
I use ramping-vus for a closed model where a population completes journeys and starts new work after completion and pacing. I use ramping-arrival-rate when new work arrives independently of previous response time. I base the choice on production behavior, then validate throughput or dropped iterations accordingly.
Q: Why can configured arrival rate differ from delivered arrival rate?
Arrival-rate executors need available VUs to start scheduled iterations. If iterations slow, allocated VUs are busy, or the generator is constrained, k6 can record dropped iterations. I monitor dropped work, active VUs, generator resources, and server-received demand before calling a run valid.
Q: What does p95 response time mean in k6?
For the selected metric samples, p95 is the value at or below which approximately 95 percent of samples fall. I always name the tagged operation, time window, sample count, workload, environment, and error treatment. It is not a promise that every request or complete user journey finished within that value.
Q: How do you make a k6 script reusable across environments?
I pass nonsecret values such as BASE_URL through -e and read them from __ENV. Credentials come from an approved secret store and are never committed. I keep behavior consistent across environments and document infrastructure or data differences that limit comparisons.
Q: How do you avoid turning k6 into the bottleneck?
I smoke-test first, estimate VU needs, and monitor generator CPU, memory, network, file descriptors, VU allocation, and dropped iterations. I reduce high-cardinality tags, excessive logging, large per-VU data, and unnecessary custom metrics. For larger workloads, I validate distributed execution and result aggregation separately.
Q: How would you diagnose rising latency with low application CPU?
Low CPU does not rule out a bottleneck. I inspect database and connection-pool waits, locks, queues, disk, network, runtime pauses, rate limits, downstream services, and traces. I form a hypothesis from aligned evidence and change one relevant factor in a comparable rerun.
Q: What should run in CI versus a scheduled performance environment?
CI should run short, controlled tests that catch obvious reliability regressions with a stable enough signal. Representative average-load tests can run on a schedule or release event. Stress, spike, soak, and resilience tests usually need reserved infrastructure, coordination, safety controls, and deeper analysis.
Common Mistakes
- Starting with an arbitrary VU count instead of a business objective and workload model.
- Sending load to a public or production system without explicit authorization, safe limits, and an abort plan.
- Treating k6 as Node.js and importing unsupported Node built-ins or npm packages into the test runtime.
- Skipping the one-iteration smoke test, then debugging data, script, network, and product failures at full load.
- Assuming failed checks fail the process without a threshold on
checksor another relevant metric. - Using a closed VU model for independent arrivals, then overlooking throughput collapse as latency rises.
- Using an arrival-rate model without watching
dropped_iterationsand generator resource saturation. - Tagging each dynamic URL, user, or order uniquely and creating excessive metric cardinality.
- Sharing one account unintentionally, which can create artificial locking, caching, or rate limiting.
- Copying tutorial thresholds into CI without an agreed objective, representative environment, and owner.
- Reading average response time alone while ignoring percentiles, throughput, failures, and business correctness.
- Claiming a server bottleneck from client metrics without aligned system evidence and a controlled follow-up.
- Comparing runs that changed the build, workload, data, cache state, environment, or aggregation method.
- Rerunning an unstable performance gate until it passes instead of preserving evidence and finding the source of variation.
Conclusion
This k6 load testing tutorial gives you a defensible path from installation to automation: define the question, use an authorized target, prove one iteration, select the executor that matches demand, add checks and thresholds, validate delivered load, and correlate k6 metrics with the system. Current k6 v2 keeps the core scripting model approachable while providing scenarios, lifecycle functions, custom metrics, dashboards, outputs, and reliable exit behavior for engineering workflows.
Your next step is to run the local smoke and order examples unchanged, deliberately break one check to prove the gate, then replace the local journey with one approved service operation. Document every workload assumption and compare one controlled baseline with one controlled change. That small, reproducible experiment is more valuable than an enormous test whose demand or evidence cannot be explained.
Interview Questions and Answers
What is the difference between checks and thresholds in k6?
Checks evaluate boolean functional conditions and emit pass or fail samples, but failures do not stop the iteration or fail the process by themselves. Thresholds evaluate aggregate metric criteria and determine the test outcome. I add a threshold on `checks` when functional correctness must gate CI.
How do you choose between ramping-vus and ramping-arrival-rate?
I choose from the demand model. Ramping VUs represents a closed user population whose iteration rate depends on response time and pacing. Ramping arrival rate represents independently arriving work, and I monitor dropped iterations and VU allocation to prove the schedule was delivered.
What does dropped_iterations mean in k6?
It counts iterations that an arrival-rate executor intended to start but could not. Common causes are insufficient preallocated or maximum VUs, slow iterations, or a constrained generator. Any dropped work requires investigation before I claim the configured arrival rate was tested.
How do you parameterize large test data efficiently in k6?
I use approved synthetic data and load array-shaped files through `SharedArray` in init context. I select records using execution context identifiers according to the required uniqueness and reuse model. I also define data exhaustion, collisions, cleanup, and distributed allocation explicitly.
How do you interpret p95 latency from a k6 result?
It is the value at or below which approximately 95 percent of the selected samples fall. I report the operation tag, analysis interval, sample count, workload, build, environment, and how failures and timeouts were treated. I pair it with useful throughput, error rate, checks, and server evidence.
How do you ensure the k6 generator is not the bottleneck?
I monitor generator CPU, memory, network, connections or file descriptors, VU allocation, scheduling, and dropped iterations. I compare planned demand with completed operations and server-received traffic. For a distributed test, I also validate consistent configuration, data partitioning, clocks, and result aggregation.
What is coordinated omission, and how does it relate to k6?
Coordinated omission occurs when a generator or measurement approach stops representing arrivals during slow periods and underreports the delay users would face. Arrival-rate executors address one common source by scheduling starts independently of completion. Generator saturation, dropped work, timeouts, and measurement boundaries still require validation.
How would you integrate k6 into CI without creating flaky gates?
I begin with a short, low-risk workload in a controlled environment and use thresholds tied to agreed criteria. I version the script, data rules, build, and environment, preserve failure evidence, and assign an owner. Larger capacity or endurance tests run on coordinated schedules rather than noisy shared pull-request runners.
Frequently Asked Questions
Is k6 free for load testing?
The k6 command-line tool is open source and can run locally or in infrastructure you manage. Grafana Cloud k6 is a separate managed offering, so review its current official service details for cloud execution needs rather than assuming local and managed features are identical.
Can I write k6 tests in JavaScript?
Yes. k6 tests use JavaScript or TypeScript syntax and k6-provided modules such as `k6/http`, `k6`, and `k6/metrics`. The runtime is not Node.js, so Node built-ins and arbitrary npm packages are not automatically supported.
How do I run a k6 load test locally?
Install k6, save a script with an exported default function, and run `k6 run script.js`. Begin with one VU and one iteration against a system you own, validate requests and checks, then apply the approved scenario profile.
Do failed k6 checks fail the test?
No, failed checks are recorded but do not by themselves cause a failed exit status. Add a threshold such as `checks: ['rate>0.99']` when the aggregate check result must fail the run.
What is the difference between VUs and iterations in k6?
A virtual user is an execution context that repeatedly runs scenario code, while an iteration is one complete execution of that function. One iteration may contain multiple HTTP requests and pacing, so neither value automatically equals requests per second or real users.
Which k6 executor should I use for API load testing?
Use a VU-based executor when the workload represents a closed population whose next action follows completion. Use an arrival-rate executor when new API work arrives independently. Validate achieved throughput for VU tests and dropped iterations for arrival-rate tests.
Can k6 generate an HTML report?
Yes. Current k6 includes a web dashboard that can export a self-contained HTML report. Set `K6_WEB_DASHBOARD=true` and `K6_WEB_DASHBOARD_EXPORT=report.html` when running the script.
Should k6 load tests run on every pull request?
A short, controlled smoke or small performance check can run on pull requests if the environment is stable enough. Capacity, stress, spike, soak, and resilience tests usually belong in coordinated scheduled or release workflows with reserved resources and clear owners.