Performance Testing interview question
Write a k6 load test with scenarios, checks and thresholds
Short answer
Define scenarios with an executor such as ramping-arrival-rate, use checks to validate responses without stopping the test, and set thresholds on metrics like p(95) of http_req_duration and http_req_failed so the run exits non-zero in CI when targets are missed. Tag requests to get per-endpoint results.
Answer
k6 scripts are JavaScript, executed by k6’s own Go-based runtime (not Node.js). The important concepts are scenarios (how load is generated), checks (per-response validation) and thresholds (pass/fail criteria for the whole run).
Example
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
const BASE_URL = __ENV.BASE_URL || 'https://test.example.com';
// Loaded once and shared read-only between VUs
const products = new SharedArray('products', () => JSON.parse(open('./products.json')));
export const options = {
scenarios: {
browse_and_search: {
executor: 'ramping-arrival-rate',
startRate: 5,
timeUnit: '1s',
preAllocatedVUs: 50,
maxVUs: 200,
stages: [
{ target: 50, duration: '2m' }, // ramp to 50 iterations/s
{ target: 50, duration: '10m' }, // hold at peak
{ target: 0, duration: '1m' },
],
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
'http_req_duration{name:product}': ['p(95)<300', 'p(99)<800'],
'http_req_duration{name:search}': ['p(95)<500'],
checks: ['rate>0.99'],
},
};
export default function () {
const product = products[Math.floor(Math.random() * products.length)];
const res = http.get(`${BASE_URL}/api/products/${product.id}`, {
tags: { name: 'product' },
});
check(res, {
'product status is 200': (r) => r.status === 200,
'product id matches': (r) => r.json('id') === product.id,
});
sleep(Math.random() * 2 + 1); // think time 1–3 s
const search = http.get(`${BASE_URL}/api/search?q=${encodeURIComponent(product.name)}`, {
tags: { name: 'search' },
});
check(search, { 'search status is 200': (r) => r.status === 200 });
}
Run it with k6 run -e BASE_URL=https://staging.example.com script.js.
What each part does
- Scenario and executor:
ramping-arrival-rateis an open model: k6 starts iterations at the target rate regardless of response time, allocating VUs from the pool as needed. If it runs out ofmaxVUs, k6 reports dropped iterations, which itself tells you the system (or the generator) could not keep up.ramping-vusor simplestagesgive a closed model instead. - Checks record pass/fail rates but never stop or fail the test on their own. That is why a
checksthreshold is included: without it, a run where every response is a 500 page could still “pass” on latency. - Thresholds are the pass/fail criteria. If any threshold is breached, k6 exits with a non-zero code, which fails the CI step.
abortOnFailcan stop a run early when a threshold is clearly broken. - Tags (
name) give per-endpoint metrics and thresholds. They also prevent URLs with IDs from being reported as thousands of separate entries. SharedArraykeeps large data files memory-efficient across VUs.
Running in CI
Short smoke-level performance tests (a few minutes, low rate) can run on every merge with thresholds as a gate, catching obvious regressions. Full load tests need a production-like environment and usually run on a schedule or before release. Export results (for example --out json=results.json, or to Prometheus or Grafana) to compare runs over time.
What interviewers look for
- Correct use of scenarios and executors, ideally knowing arrival-rate vs VU-based models.
- The difference between checks and thresholds.
- Percentile and error-rate thresholds, tagging, and how the exit code gates CI.
A strong answer covers
- Scenarios and executors control the load shape (VUs vs arrival rate)
- Checks validate responses but do not fail the run by themselves
- Thresholds define pass/fail and drive the exit code
- Percentile and error-rate metrics, tagged per endpoint
- Think time, test data and running in CI