k6 expresses load checks as code that can live beside an application and run repeatably. A normal HTTP script creates network requests without rendering the UI. API latency helps evaluate services but is distinct from time until a user sees a ready page.
The tool is useful when teams explicitly define load profiles and success criteria. A ten-line script is easy to run, but a meaningful performance test also requires correct data, response checks, server observation, and generator monitoring. Those conditions make results useful for engineering decisions.
Start a scenario with a requirement
Describe the operation and context: for example, a catalog must handle an agreed read flow at a specified product volume. Define error rate, acceptable delays, steady-state duration, and cache state before measurement.
Avoid adopting arbitrary example values as product requirements. Five minutes at low intensity can validate script mechanics but cannot establish absence of memory leaks, peak-sale behavior, or whole-application capacity.
Choosing a load model
A closed model has a fixed virtual-user population repeating operations. Slower responses reduce new-iteration frequency. This suits some bounded-user populations but can hide incoming traffic that continues independently of server state.
The constant-arrival-rate executor specifies iteration start frequency, which is not automatically request frequency: an iteration may contain several requests. Sustaining it requires available VUs and resources. If insufficient, iterations are not started; analyze dropped_iterations alongside latency.
A minimal HTTP script
Here, your own /api/catalog endpoint must return an object containing an items array. Save the code as catalog.js. Replace illustrative thresholds with project criteria. The script requires installed k6 and an accessible agreed test environment.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
catalog: {
executor: 'constant-arrival-rate',
rate: 5,
timeUnit: '1s',
duration: '1m',
preAllocatedVUs: 10,
maxVUs: 20,
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
checks: ['rate>0.99'],
dropped_iterations: ['count==0'],
},
};
export default function () {
const res = http.get(`${__ENV.BASE_URL}/api/catalog`, {
timeout: '5s',
tags: { name: 'catalog' },
});
let valid = false;
try {
valid = Array.isArray(res.json().items);
} catch (_) {
valid = false;
}
check(res, {
'catalog status is 200': (r) => r.status === 200,
'catalog contains items array': () => valid,
});
}k6 run -e BASE_URL=https://qa.example.test catalog.jsStart at lower intensity if the environment's resource budget is unknown. For arrival-rate execution, do not add sleep merely to maintain frequency; the executor already controls starts. Add waits within actual business journeys only when the model calls for them.
Checks and thresholds
A check records one assertion's outcome. Failure alone does not fail the whole run under acceptance rules. A threshold defines a metric criterion and can produce a failed run when violated.
The example checks transport outcome and JSON structure. A checks threshold connects assertions to run acceptance; http_req_failed measures something different. Use operation-specific tags and thresholds so a fast catalog cannot hide slow order creation in aggregate statistics.
Data and step relationships
For login and cart flows, extract current tokens and object IDs from responses. Do not reuse one recorded session across every user. Correlation errors turn useful business traffic into widespread authentication failures or contention on one object.
Plan account allocation and data lifecycle. Creating orders requires unique values, final-state checks, and safe cleanup. For a read-only catalog, vary queries with a meaningful distribution; repeatedly reading one hot record mainly tests its cache.
Preparing an experiment
Record the build, configuration, database size, generator location, and cache state. Run a single check and a small trial. Agree on stop conditions for unacceptable errors or impact on other teams.
Separate warm-up and measurement. Initial connections and cache population affect results. If cold start itself is under investigation, keep it as a separate scenario rather than excluding it retrospectively.
Observing resources
Monitor generator CPU, memory, network, and unstarted iterations. A generator unable to create the target flow cannot establish server capacity. Increasing maxVUs may only increase memory use without resolving a network limit.
Observe application CPU, memory, queues, database, and connection pools. Correlate their intervals with k6 metrics. Rising p95 alongside connection waits suggests a next diagnostic check. Without server evidence, “the database is slow” remains a hypothesis.
Reading results
Check achieved frequency, successful assertions, errors, and latency distributions per operation. Low latency with many failures may simply mean rapid rejection. Retain each threshold violation's cause.
For this example, expect agreed JSON, no dropped_iterations, and predefined thresholds met during measurement. Success describes only catalog reads under those conditions, without establishing UI speed, payment behavior, or performance at much larger data volumes.
Common mistakes and CI use
Common mistakes include treating checks alone as a quality gate, confusing VUs with RPS, checking only status, and hiding generation failures. Results from an arbitrary laptop also cannot be transferred to production infrastructure without assessing differences.
CI benefits from a small stable scenario with a clear resource budget and retained report. Run heavier experiments in dedicated windows. k6 then provides reproducible requirement verification with known conditions, failure reasons, and practical limits.