Lesson 1 of 9 · Advanced Quality Engineering
Performance Testing Fundamentals
Plan and run meaningful performance tests: test types, workload models, open vs closed load, percentiles, thresholds and practical k6 scripts.
intermediate 7 min read Updated
You will learn to
- Distinguish load, stress, spike, soak and breakpoint tests
- Build a workload model from production data and choose open or closed load
- Read latency percentiles and explain why averages mislead
- Write k6 scripts with checks, stages, scenarios and pass/fail thresholds
Prerequisites
- HTTP fundamentals
- Basic JavaScript
Functional tests ask “does it work?”. Performance tests ask “does it keep working, fast enough, under realistic and extreme load, for long enough?”. The hard part is rarely the tool. It is deciding what load to apply, what to measure and what counts as a pass.
Types of performance test
| Type | Question it answers | Typical shape |
|---|---|---|
| Smoke / baseline | Does the script work and what is the latency at minimal load? | 1 to a few users, short |
| Load | Does the system meet its targets at expected peak traffic? | Ramp to expected peak, hold |
| Stress | How does it behave above peak, and does it degrade gracefully? | Ramp beyond peak in steps |
| Spike | Can it absorb a sudden surge and recover? | Jump to high load, drop back |
| Soak (endurance) | Do leaks or slow degradation appear over hours? | Moderate load, long duration |
| Breakpoint | At what load does it stop meeting targets? | Keep increasing until failure |
Run the smoke test first, every time. A script with a broken login will happily “load test” your error page.
Workload modelling
A workload model describes who does what, how often. Without one, you are measuring an arbitrary number.
Build it from evidence: production analytics, access logs, APM data or, for a new system, the business forecast. Capture:
- Arrival rate at peak: requests or user sessions per second.
- Transaction mix: for example 60% browse, 25% search, 10% add to cart, 5% checkout.
- Think time between user actions.
- Data variety: different users, products and search terms, so caches behave realistically.
Open versus closed models
This distinction changes your results more than almost anything else.
- Closed model: a fixed number of virtual users (VUs) loop through the script. Each VU waits for a response before sending the next request. If the system slows down, the request rate drops automatically, which can hide the problem.
- Open model: new requests arrive at a set rate regardless of how fast the system responds, like real internet traffic. If the system slows, work queues up and latency climbs, which is what real users would experience.
For public-facing systems where users arrive independently, an open model is usually more realistic. Closed models suit cases with a genuinely fixed population, such as a fixed number of call-centre agents.
Little’s Law as a sanity check
Little’s Law relates concurrency, throughput and time in a stable system:
concurrency = throughput × time in system
Example: 200 requests/s × 0.25 s average response = ~50 requests in flight
Use it to sanity-check results and to size the number of VUs a tool needs to sustain a target rate.
Measuring: why percentiles beat averages
Latency distributions are skewed: most requests are quick, a few are very slow. An average blends them into a number that describes no actual user.
Ten response times (ms): 90 95 100 100 105 110 110 120 130 2400
Average: 336 ms Median (p50): 107.5 ms p90: ~357 ms max: 2400 ms
The average suggests every request takes about a third of a second, while nine out of ten are around 100 ms and one is terrible. Percentiles describe the experience directly: p95 = 500 ms means 95% of requests completed in 500 ms or less. Report at least p50, p95 and p99, plus error rate and throughput.
(Different tools interpolate percentiles slightly differently on small samples; with realistic sample sizes the difference is negligible.)
Also watch the system under test, not just the client: CPU, memory, garbage collection, database connections, queue depths. Latency tells you there is a problem; resource metrics tell you where.
Writing k6 scripts
k6 is an open-source load testing tool. You write scripts in JavaScript and run them with the k6 binary (it does not run on Node.js).
A first load test
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 20,
duration: '1m',
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500 ms
http_req_failed: ['rate<0.01'], // less than 1% errors
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
const res = http.get(`${BASE_URL}/`);
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1); // think time
}
k6 run -e BASE_URL=http://localhost:3000 load-test.js
__ENV exposes variables passed with -e, so the same script can target a local build or a staging environment without edits.
Checks record pass rates but do not fail the run on their own. Thresholds are the pass/fail criteria: if any threshold is crossed, k6 exits with a non-zero code, which fails a CI job. That is what makes performance tests usable as automated gates.
Ramping with stages
export const options = {
stages: [
{ duration: '2m', target: 50 }, // ramp up
{ duration: '5m', target: 50 }, // hold at expected peak
{ duration: '2m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1500'],
},
};
stages ramps VUs, which is a closed model.
An open model with scenarios
To model arrivals rather than users, use an arrival-rate executor:
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
browse: {
executor: 'constant-arrival-rate',
rate: 100, // 100 iterations...
timeUnit: '1s', // ...per second
duration: '5m',
preAllocatedVUs: 50,
maxVUs: 300,
},
},
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
const res = http.get(`${BASE_URL}/products`);
check(res, { 'status is 200': (r) => r.status === 200 });
}
k6 starts iterations at the requested rate and allocates VUs as needed. If it runs out of VUs, it reports dropped iterations, which is itself a signal that the system (or your load generator) is saturated.
Making results trustworthy
- Production-like environment: a test against a laptop-sized staging database tells you little about production. Document the differences.
- Warm-up: exclude or separate the first minutes when caches and JIT compilers are cold.
- Load generator health: if the machine running k6 is at 100% CPU, you are measuring the generator. Monitor it.
- Repeatability: run the same test several times. If p95 varies wildly between identical runs, find out why before drawing conclusions.
- Change one thing at a time when tuning, and keep a log of runs, versions and configurations.
- Correlate: line up latency graphs with server metrics and deployment times.
Practice exercise
Analytics show a peak of about 30 checkout requests per second, and the product owner wants p95 checkout latency under 800 ms with less than 0.5% errors. Write a k6 options block for a 10-minute load test that models arrivals realistically and fails if the targets are missed. Then name two things you would check before trusting the result.
Show a model answer
export const options = {
scenarios: {
checkout_peak: {
executor: 'constant-arrival-rate',
rate: 30,
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 40,
maxVUs: 200,
},
},
thresholds: {
http_req_duration: ['p(95)<800'],
http_req_failed: ['rate<0.005'],
},
};An arrival-rate executor is used because shoppers arrive independently: an open model keeps sending 30 requests per second even if the service slows, exposing queueing that a fixed-VU test could hide.
Before trusting the result:
- Run a smoke test and confirm checks pass, so the script is really completing checkouts (with valid, varied test data) and not hitting an error page.
- Confirm the load generator was not saturated and there were no dropped iterations, and that the environment and data volumes are comparable to production.
Key takeaways
- Pick the test type from the question you need answered: load, stress, spike, soak or breakpoint.
- Base load on a workload model from real data: arrival rate, transaction mix, think time and data variety.
- Open models (arrival rate) expose queueing that closed models (fixed VUs) can hide.
- Report percentiles (p50, p95, p99), error rate and throughput; averages mislead.
- In k6, checks observe and thresholds decide pass or fail, which makes performance tests CI gates.
- Only load test systems you are authorised to test, and validate the environment and generator before trusting numbers.
Key terms
Finished this lesson?
Progress is saved in this browser. No account needed.