The ideas in plain words, then 5 hands-on k6 exercises and a ready JMeter plan. Everything runs against the practice API on your machine, so you can push it as hard as you like without harming anyone.
⚠️ The golden rule: only load test systems you own or have written permission to test.
A load test is a flood of requests. Pointed at someone else's site — including this one — it can slow it down for real users, cost the owner money, break their terms of service and look like an attack. Use the practice API below, or your own team's test environment with permission.
Free and open source from Grafana. Follow the official install guide (Homebrew, winget, apt or a download), then check it:
k6 version
Step 3
Save a script and run it
Copy an exercise below into a file (for example smoke.js) and run it. k6 prints a summary when it finishes.
k6 run smoke.js
Types of performance test
Type
What it is
When
Typical shape
Smoke
A tiny run (1 user) to prove the script and the system work at all.
Before every other test
1 user · 30 s
Load
The traffic you expect on a normal busy day.
Before each release
20 users · 1–2 min
Stress
Keep adding users until something breaks — then see how it breaks.
To find the limit
Ramp to 200+ users
Spike
A sudden jump in users, like a sale starting.
Flash sales, launches
5 → 100 users in 5 s
Soak
Normal load for a long time, to find slow leaks (memory, connections).
Before big launches
20 users · 1–8 hours
Words you'll see in every report
Response time (p95)
95% of requests were faster than this. Averages hide slow requests — percentiles don't.
Throughput
How many requests per second the system handled.
Error rate
The share of requests that failed. Fast but wrong is still a failure.
Virtual users (VUs)
Simulated people using the system at the same time.
Think time
Pauses between a user's actions (sleep). Without it, 10 VUs behave like hundreds of real people.
Thresholds
Your pass/fail rules, e.g. “p95 under 200 ms and errors under 1%”. They turn a load test into a test.
Exercises with k6
Each script was run against the practice API by our test suite. Start with the smoke test.
1. Smoke test
Goal: One user for 30 seconds. Check the script works and every response is correct before you add load.
smoke.js · k6 run smoke.js
import http from'k6/http';
import { check, sleep } from'k6';
exportconst options = {
vus: 1,
duration: '30s',
thresholds: {
http_req_failed: ['rate<0.01'], // under 1% errors
http_req_duration: ['p(95)<200'], // 95% of requests under 200 ms
},
};
export default function () {
const res = http.get('http://localhost:4000/api/products?search=a');
check(res, {
'status is 200': (r) => r.status === 200,
'has products': (r) => r.json('total') > 0,
});
sleep(1); // think time
}
👀 What to look for: checks 100% ✓, http_req_failed 0.00%, and both thresholds ✓. If anything fails here, fix it before going further.
2. Load test with ramp-up
Goal: Ramp up to 20 users, hold, then ramp down — the shape of a normal busy period.
load.js · k6 run load.js
import http from'k6/http';
import { check, sleep } from'k6';
exportconst options = {
stages: [
{ duration: '15s', target: 20 }, // ramp up to 20 users
{ duration: '30s', target: 20 }, // hold
{ duration: '10s', target: 0 }, // ramp down
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<200'],
},
};
const pages = ['/api/products', '/api/products?category=books', '/api/products?sort=-price', '/api/products/1'];
export default function () {
const url = 'http://localhost:4000' + pages[Math.floor(Math.random() * pages.length)];
const res = http.get(url);
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(Math.random() * 2 + 0.5); // 0.5–2.5 s think time, like real people
}
👀 What to look for: http_req_duration p(95) stays under 200 ms while VUs climb to 20, and http_reqs (throughput) rises with the users.
3. Find the slow endpoint
Goal: One request type is slow (we fake it with the X-Delay header). Tag each request and set a threshold per tag, so the report tells you which one broke the rules.
slow-dependency.js · k6 run slow.js
import http from'k6/http';
import { check, sleep } from'k6';
exportconst options = {
vus: 10,
duration: '20s',
thresholds: {
'http_req_duration{name:products}': ['p(95)<200'],
'http_req_duration{name:reviews}': ['p(95)<200'], // this one will fail
},
};
export default function () {
const fast = http.get('http://localhost:4000/api/products', { tags: { name: 'products' } });
// pretend a slow service sits behind this call: the server waits 350 ms before answeringconst slow = http.get('http://localhost:4000/api/products/1', { headers: { 'X-Delay': '350' }, tags: { name: 'reviews' } });
check(fast, { 'products 200': (r) => r.status === 200 });
check(slow, { 'reviews 200': (r) => r.status === 200 });
sleep(0.5);
}
👀 What to look for: The run fails (k6 exits with an error) and the summary marks ✗ on http_req_duration{name:reviews} while {name:products} passes. That's how you point developers at the right endpoint.
4. Spike test
Goal: Jump from 5 to 100 users in 5 seconds, then drop back — like a sale going live. Does it stay up and recover?
spike.js · k6 run spike.js
import http from'k6/http';
import { check, sleep } from'k6';
exportconst options = {
stages: [
{ duration: '10s', target: 5 }, // normal
{ duration: '5s', target: 100 }, // spike!
{ duration: '15s', target: 100 },
{ duration: '5s', target: 5 }, // back to normal
{ duration: '10s', target: 5 }, // did it recover?
],
thresholds: { http_req_failed: ['rate<0.01'] },
};
export default function () {
const res = http.get('http://localhost:4000/api/products?sort=-rating');
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
👀 What to look for: Compare response times during the spike with before and after. Errors or a slow recovery after the spike are findings worth reporting.
5. A real user journey (with login)
Goal: Log in once in setup(), then each user browses and places an order. Under load, some orders will fail with 409 Out of stock — that's expected behaviour, not an error.
journey.js · k6 run journey.js
import http from'k6/http';
import { check, sleep } from'k6';
constBASE = 'http://localhost:4000';
exportconst options = {
vus: 10,
duration: '30s',
thresholds: {
http_req_failed: ['rate<0.01'],
'http_req_duration{name:place-order}': ['p(95)<300'],
},
};
// 409 (out of stock) is a correct answer under load — don't count it as a failure
http.setResponseCallback(http.expectedStatuses(200, 201, 409));
exportfunction setup() {
http.post(`${BASE}/api/reset`); // start from fresh dataconst res = http.post(`${BASE}/api/auth/login`, JSON.stringify({ username: 'ravi', password: 'Ravi@123' }),
{ headers: { 'Content-Type': 'application/json' } });
return { token: res.json('token') }; // shared with every virtual user
}
export default function ({ token }) {
const list = http.get(`${BASE}/api/products?inStock=true&limit=5`, { tags: { name: 'browse' } });
const product = list.json('data.0');
if (!product) return; // everything is sold out
sleep(1);
const res = http.post(`${BASE}/api/orders`, JSON.stringify({ items: [{ productId: product.id, quantity: 1 }] }), {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
'Idempotency-Key': `order-vu${__VU}-it${__ITER}`, // unique per attempt (8–64 characters)
},
tags: { name: 'place-order' },
});
check(res, {
'order placed or sold out': (r) => r.status === 201 || r.status === 409,
'total is right': (r) => r.status !== 201 || r.json('total') === product.price,
});
sleep(1);
}
👀 What to look for: http_req_failed stays at 0% because 409 is declared an expected status. Also check the order checks: a fast API that returns wrong totals would still fail them.
Prefer JMeter?
A ready test plan: 20 shoppers search and view products for 30 seconds, with think time, a status check and a 500 ms response-time check. Open it in the JMeter GUI to see how it's built.
# 1. Download practiceqa-load.jmx (button above) and Apache JMeter (jmeter.apache.org)# 2. Start the practice API, then run JMeter without the GUI (the GUI is only for editing):
jmeter -n -t practiceqa-load.jmx -l results.jtl -e -o report
# Change the load without editing the file:
jmeter -n -t practiceqa-load.jmx -Jusers=50 -Jduration=60 -Jport=4000 -l results.jtl -e -o report
# 3. Open report/index.html for charts of response times, throughput and errors
Good habits
Smoke first. Never start a big run with a script you haven't seen pass with one user.
Check correctness, not only speed. Assert status codes and key fields — a fast wrong answer is a failure.
Use percentiles and thresholds. Agree targets (e.g. p95 < 200 ms, errors < 1%) before the run, so the result is pass or fail.
Add think time and unique data, so virtual users behave like people and don't collide on the same records.
Know your limits: here the load tool and the API share one computer, so results show trends, not production capacity. Real capacity tests run from separate machines against a production-like environment.
Change one thing at a time and compare runs — performance work is science, not guessing.
Next: practice functional API testing on the same server — Practice REST API.