K6 Performance Testing
A comprehensive guide to load testing and performance testing with Grafana K6. Learn how to write, run, and analyze performance tests for your APIs and services using K6's developer-friendly JavaScript scripting engine.
Introduction
What is K6?
K6 is an open-source load testing tool built by Grafana Labs. It lets you write performance tests in JavaScript, execute them from the command line, and analyze results with built-in metrics, thresholds, and integrations. K6 is designed for developers and QA engineers who want to shift performance testing left — making it part of the development workflow rather than an afterthought.
Why Use K6 for Performance Testing?
- Developer-friendly — Write tests in JavaScript (ES6 modules). If you can write JS, you can write K6 tests. No XML, no GUIs, no proprietary languages.
- CLI-first — K6 runs entirely from the command line, making it trivial to integrate into CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins, etc.).
- Powerful metrics engine — Built-in metrics for HTTP timings, throughput, error rates, and more. Create custom metrics for business-specific KPIs.
- Thresholds as code — Define pass/fail criteria directly in your test script. If thresholds are breached, K6 exits with a non-zero code — perfect for automated quality gates.
- Multiple load patterns — Constant VUs, ramping VUs, constant arrival rate, externally controlled — model any real-world traffic pattern.
- Lightweight and fast — Written in Go, K6 can generate massive load from a single machine. No JVM overhead, no heavy resource consumption.
How Does K6 Compare?
| Feature | K6 | JMeter | Gatling |
|---|---|---|---|
| Scripting Language | JavaScript (ES6) | XML / GUI | Scala / Java |
| Learning Curve | Low (JS knowledge) | Medium (GUI-based) | Medium (Scala DSL) |
| CI/CD Integration | Native CLI, easy | Possible with plugins | Good (Maven/Gradle) |
| Resource Usage | Lightweight (Go-based) | Heavy (JVM) | Moderate (JVM) |
| Protocol Support | HTTP, WebSocket, gRPC | HTTP, JDBC, JMS, LDAP, FTP | HTTP, WebSocket, JMS |
| Real-time Results | CLI output + streaming to dashboards | GUI + listeners | HTML report after run |
| Browser Testing | K6 browser module (Chromium) | Not natively | Not natively |
| Cloud Option | Grafana Cloud K6 | BlazeMeter | Gatling Enterprise |
Installation
K6 is a standalone binary — no runtime dependencies, no JVM, no Node.js required. You can install it via package managers, Docker, or by downloading the binary directly.
1# macOS / Linux via Homebrew
2brew install k6
3
4# Windows via Chocolatey
5choco install k6
6
7# Windows via winget
8winget install k6 --source winget
9
10# Docker (no installation needed)
11docker run --rm -i grafana/k6 run - <script.js
12
13# Download the official binary manually
14# https://github.com/grafana/k6/releasesVerify Installation
After installation, verify K6 is working by checking its version:
1# Check K6 version
2k6 version
3
4# Expected output:
5# k6 v0.49.0 (go1.22.0, linux/amd64)grafana/k6 image contains everything needed. Mount your test scripts as a volume: docker run --rm -v $(pwd):/scripts grafana/k6 run /scripts/test.jsWriting Your First Test
A K6 test script is a standard JavaScript ES6 module. It must export a default function, which is the entry point that each virtual user (VU) executes in a loop. The options export configures how the test runs.
1import http from 'k6/http';
2import { sleep } from 'k6';
3
4// Options configure how the test runs
5export const options = {
6 // Simulate 10 virtual users
7 vus: 10,
8 // Run for 30 seconds
9 duration: '30s',
10};
11
12// The default function is the entry point for each virtual user.
13// Every VU executes this function in a loop for the entire duration.
14export default function () {
15 // Make a GET request
16 http.get('https://test-api.k6.io/public/crocodiles/');
17
18 // Pause for 1 second between iterations (simulates user think time)
19 sleep(1);
20}Running the Test
Use the k6 run command to execute your script. You can override options via CLI flags, which is convenient for quick experiments.
1# Run the test
2k6 run script.js
3
4# Run with more virtual users (override options)
5k6 run --vus 50 --duration 1m script.js
6
7# Run and output results to a JSON file
8k6 run --out json=results.json script.js
9
10# Run inside Docker
11docker run --rm -i grafana/k6 run - <script.jsUnderstanding the Output
After a test completes, K6 prints a summary of all collected metrics. Here are the most important built-in metrics:
- http_req_duration — Total time for the request (DNS + connect + TLS + sending + waiting + receiving). This is the primary latency metric.
- http_req_failed — The rate of requests that returned a non-2xx/3xx status code.
- http_reqs — Total number of HTTP requests generated.
- iterations — How many times the default function completed across all VUs.
- vus — Current number of active virtual users.
- data_received / data_sent — Total amount of network data transferred.
HTTP Requests
K6 provides the http module for making HTTP requests. It supports all standard methods (GET, POST, PUT, PATCH, DELETE) along with custom headers, authentication, and request tagging for granular metric analysis.
GET Requests
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4export const options = {
5 vus: 5,
6 duration: '10s',
7};
8
9export default function () {
10 // Simple GET request
11 const res = http.get('https://test-api.k6.io/public/crocodiles/');
12
13 // GET with custom headers
14 const params = {
15 headers: {
16 'Accept': 'application/json',
17 'Authorization': 'Bearer my-token-here',
18 },
19 tags: { name: 'GetCrocodiles' },
20 };
21 const resWithHeaders = http.get('https://test-api.k6.io/public/crocodiles/', params);
22
23 // GET with query parameters
24 const resFiltered = http.get('https://test-api.k6.io/public/crocodiles/?format=json');
25
26 sleep(1);
27}POST Requests
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4export const options = {
5 vus: 5,
6 duration: '10s',
7};
8
9export default function () {
10 const url = 'https://test-api.k6.io/auth/token/login/';
11
12 // POST with JSON body
13 const payload = JSON.stringify({
14 username: 'testuser',
15 password: 'superSecure123!',
16 });
17
18 const params = {
19 headers: {
20 'Content-Type': 'application/json',
21 },
22 };
23
24 const res = http.post(url, payload, params);
25
26 check(res, {
27 'login succeeded': (r) => r.status === 200,
28 'received access token': (r) => r.json('access') !== undefined,
29 });
30
31 sleep(1);
32}PUT, PATCH, and DELETE
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4export default function () {
5 const baseUrl = 'https://test-api.k6.io/my/crocodiles/';
6 const headers = {
7 'Content-Type': 'application/json',
8 'Authorization': 'Bearer <token>',
9 };
10
11 // PUT - Full update of a resource
12 const putPayload = JSON.stringify({
13 name: 'Updated Croc',
14 sex: 'M',
15 date_of_birth: '1990-01-01',
16 });
17
18 const putRes = http.put(`${baseUrl}1/`, putPayload, { headers });
19
20 check(putRes, {
21 'PUT status is 200': (r) => r.status === 200,
22 'name was updated': (r) => r.json('name') === 'Updated Croc',
23 });
24
25 // PATCH - Partial update of a resource
26 const patchPayload = JSON.stringify({ name: 'Patched Croc' });
27 const patchRes = http.patch(`${baseUrl}1/`, patchPayload, { headers });
28
29 check(patchRes, {
30 'PATCH status is 200': (r) => r.status === 200,
31 });
32
33 // DELETE - Remove a resource
34 const delRes = http.del(`${baseUrl}1/`, null, { headers });
35
36 check(delRes, {
37 'DELETE status is 204': (r) => r.status === 204,
38 });
39
40 sleep(1);
41}JSON.stringify() for request bodies and set the Content-Type header to application/json. K6 does not automatically serialize objects like Axios or Fetch do in Node.js.require(), npm packages, or Node.js built-in modules. Use K6's built-in modules (k6/http, k6/crypto, k6/encoding, etc.) instead. If you need external libraries, use K6 extensions or bundle them with webpack/esbuild.Checks and Thresholds
Checks are assertions that validate individual response properties. They do not stop the test on failure — they record pass/fail rates. Thresholds are pass/fail criteria for the entire test run. If a threshold is breached, K6 exits with code 99, making it ideal for CI/CD quality gates.
Checks
Use check() to validate response status codes, body content, headers, and response times. Checks appear in the output summary as a pass/fail percentage.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4export const options = {
5 vus: 10,
6 duration: '30s',
7};
8
9export default function () {
10 const res = http.get('https://test-api.k6.io/public/crocodiles/');
11
12 // Checks validate response properties.
13 // They do NOT abort the test on failure -- they simply track pass/fail rates.
14 check(res, {
15 // Status code check
16 'status is 200': (r) => r.status === 200,
17
18 // Response time check
19 'response time < 500ms': (r) => r.timings.duration < 500,
20
21 // Body content checks
22 'body is not empty': (r) => r.body.length > 0,
23 'body contains crocodile data': (r) => {
24 const body = r.json();
25 return Array.isArray(body) && body.length > 0;
26 },
27
28 // Header checks
29 'content-type is JSON': (r) =>
30 r.headers['Content-Type'].includes('application/json'),
31 });
32
33 sleep(1);
34}Thresholds
Thresholds define the performance criteria your system must meet. If any threshold is breached, the test is considered a failure. This is the primary mechanism for automated performance gates in CI/CD.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4export const options = {
5 vus: 20,
6 duration: '1m',
7
8 // Thresholds define pass/fail criteria for the entire test.
9 // If any threshold is breached, k6 exits with a non-zero code (great for CI/CD).
10 thresholds: {
11 // 95% of requests must complete below 500ms
12 http_req_duration: ['p(95)<500'],
13
14 // 99% of requests must complete below 1500ms
15 'http_req_duration': ['p(95)<500', 'p(99)<1500'],
16
17 // Request failure rate must be below 1%
18 http_req_failed: ['rate<0.01'],
19
20 // At least 95% of checks must pass
21 checks: ['rate>0.95'],
22
23 // Custom threshold on a specific request using tags
24 'http_req_duration{name:GetCrocodiles}': ['p(95)<400'],
25 },
26};
27
28export default function () {
29 const res = http.get('https://test-api.k6.io/public/crocodiles/', {
30 tags: { name: 'GetCrocodiles' },
31 });
32
33 check(res, {
34 'status is 200': (r) => r.status === 200,
35 'response time OK': (r) => r.timings.duration < 500,
36 });
37
38 sleep(1);
39}Threshold Syntax Reference
| Expression | Meaning |
|---|---|
p(95)<500 | 95th percentile must be under 500ms |
p(99)<1500 | 99th percentile must be under 1500ms |
avg<200 | Average value must be under 200ms |
max<3000 | Maximum value must be under 3000ms |
med<300 | Median (50th percentile) under 300ms |
rate<0.01 | Rate metric must be under 1% |
rate>0.95 | Rate metric must be above 95% |
count<100 | Counter must be under 100 |
checks metric (e.g., checks: ['rate>0.95']) to fail the test if too many checks fail.Load Test Types
Different testing scenarios require different load profiles. K6's stages option lets you define how virtual users ramp up, sustain, and ramp down over time. Here are the five fundamental load test types every performance engineer should know.
Smoke Test
A minimal test with 1 user to verify the system works correctly under zero load. Run this first before any heavier tests. If the smoke test fails, there is no point running larger tests.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4// Smoke Test: Verify the system works with minimal load.
5// Run 1 user for 1 minute to catch basic failures.
6export const options = {
7 vus: 1,
8 duration: '1m',
9 thresholds: {
10 http_req_failed: ['rate<0.01'],
11 http_req_duration: ['p(95)<600'],
12 },
13};
14
15export default function () {
16 const res = http.get('https://test-api.k6.io/public/crocodiles/');
17 check(res, { 'status is 200': (r) => r.status === 200 });
18 sleep(1);
19}Load Test
Simulates expected normal traffic. Use stages to gradually ramp up users, sustain the target load, and ramp down. This validates that the system meets SLAs under typical conditions.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4// Load Test: Assess performance under expected normal load.
5// Ramp up to 100 users, sustain, then ramp down.
6export const options = {
7 stages: [
8 { duration: '2m', target: 50 }, // Ramp up to 50 users over 2 min
9 { duration: '5m', target: 50 }, // Stay at 50 users for 5 min
10 { duration: '2m', target: 100 }, // Ramp up to 100 users over 2 min
11 { duration: '5m', target: 100 }, // Stay at 100 users for 5 min
12 { duration: '2m', target: 0 }, // Ramp down to 0 over 2 min
13 ],
14 thresholds: {
15 http_req_duration: ['p(95)<500'],
16 http_req_failed: ['rate<0.01'],
17 },
18};
19
20export default function () {
21 const res = http.get('https://test-api.k6.io/public/crocodiles/');
22 check(res, { 'status is 200': (r) => r.status === 200 });
23 sleep(1);
24}Stress Test
Gradually increases load beyond normal capacity to find the system's breaking point. The goal is to identify which component fails first (database, API, network) and how the system behaves under extreme pressure.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4// Stress Test: Push the system beyond normal load to find its breaking point.
5export const options = {
6 stages: [
7 { duration: '2m', target: 100 }, // Normal load
8 { duration: '5m', target: 100 },
9 { duration: '2m', target: 200 }, // Above normal
10 { duration: '5m', target: 200 },
11 { duration: '2m', target: 300 }, // Pushing limits
12 { duration: '5m', target: 300 },
13 { duration: '2m', target: 400 }, // Breaking point?
14 { duration: '5m', target: 400 },
15 { duration: '5m', target: 0 }, // Recovery
16 ],
17 thresholds: {
18 http_req_duration: ['p(95)<1500'], // More lenient under stress
19 http_req_failed: ['rate<0.05'], // Allow up to 5% failure
20 },
21};
22
23export default function () {
24 const res = http.get('https://test-api.k6.io/public/crocodiles/');
25 check(res, { 'status is 200': (r) => r.status === 200 });
26 sleep(1);
27}Spike Test
Simulates a sudden, massive surge in traffic — like a flash sale, viral social media post, or DDoS attack. Tests both the system's ability to handle the spike and its recovery behavior afterward.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4// Spike Test: Simulate a sudden, massive surge in traffic.
5// Tests how the system recovers after an extreme burst.
6export const options = {
7 stages: [
8 { duration: '30s', target: 10 }, // Warm up
9 { duration: '1m', target: 10 }, // Normal traffic
10 { duration: '10s', target: 500 }, // SPIKE! 500 users in 10 seconds
11 { duration: '3m', target: 500 }, // Stay at peak
12 { duration: '10s', target: 10 }, // Spike ends abruptly
13 { duration: '3m', target: 10 }, // Recovery period
14 { duration: '30s', target: 0 }, // Cool down
15 ],
16 thresholds: {
17 http_req_duration: ['p(95)<3000'], // Very lenient during spike
18 http_req_failed: ['rate<0.10'], // Allow up to 10% failure
19 },
20};
21
22export default function () {
23 const res = http.get('https://test-api.k6.io/public/crocodiles/');
24 check(res, { 'status is 200': (r) => r.status === 200 });
25 sleep(1);
26}Soak Test
Runs at normal load for an extended period (hours). The goal is to uncover issues that only appear over time: memory leaks, connection pool exhaustion, disk space filling up, database slow queries accumulating, or garbage collection pauses.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4// Soak Test: Run at normal load for an extended period.
5// Identifies memory leaks, connection pool exhaustion, and degradation over time.
6export const options = {
7 stages: [
8 { duration: '5m', target: 100 }, // Ramp up
9 { duration: '4h', target: 100 }, // Sustain for 4 hours
10 { duration: '5m', target: 0 }, // Ramp down
11 ],
12 thresholds: {
13 http_req_duration: ['p(95)<500'],
14 http_req_failed: ['rate<0.01'],
15 },
16};
17
18export default function () {
19 const res = http.get('https://test-api.k6.io/public/crocodiles/');
20 check(res, { 'status is 200': (r) => r.status === 200 });
21 sleep(1);
22}| Test Type | VUs | Duration | Goal |
|---|---|---|---|
| Smoke | 1 | 1 minute | Verify basic functionality |
| Load | 50-100 | 15-30 minutes | Validate normal traffic SLAs |
| Stress | 100-400+ | 30-60 minutes | Find the breaking point |
| Spike | 500+ (burst) | 10-15 minutes | Test sudden traffic surges |
| Soak | 50-100 | 4-12 hours | Find memory leaks and degradation |
Custom Metrics
K6 provides four types of custom metrics that let you track business-specific KPIs beyond the built-in HTTP metrics. Custom metrics can also have thresholds applied to them, enabling precise performance gates.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3import { Counter, Gauge, Rate, Trend } from 'k6/metrics';
4
5// Counter: Cumulative total that only goes up
6const totalRequests = new Counter('total_requests');
7const errorCount = new Counter('error_count');
8
9// Gauge: A value that can go up and down (current state)
10const activeItems = new Gauge('active_items');
11
12// Rate: Percentage of non-zero values (pass/fail ratio)
13const successRate = new Rate('success_rate');
14
15// Trend: Collects values and calculates statistics (min, max, avg, percentiles)
16const loginDuration = new Trend('login_duration');
17
18export const options = {
19 vus: 10,
20 duration: '30s',
21 thresholds: {
22 // You can set thresholds on custom metrics too
23 success_rate: ['rate>0.95'],
24 login_duration: ['p(95)<800', 'avg<400'],
25 error_count: ['count<10'],
26 },
27};
28
29export default function () {
30 // Track a counter
31 totalRequests.add(1);
32
33 const res = http.get('https://test-api.k6.io/public/crocodiles/');
34
35 // Track success/failure rate
36 const isSuccess = res.status === 200;
37 successRate.add(isSuccess);
38
39 if (!isSuccess) {
40 errorCount.add(1);
41 }
42
43 // Track a gauge (snapshot value)
44 const body = res.json();
45 if (Array.isArray(body)) {
46 activeItems.add(body.length);
47 }
48
49 // Track a trend (timing data)
50 loginDuration.add(res.timings.duration);
51
52 check(res, {
53 'status is 200': (r) => r.status === 200,
54 });
55
56 sleep(1);
57}Metric Types
| Type | Description | Use Case | Aggregations |
|---|---|---|---|
Counter | Cumulative sum, only goes up | Total orders placed, total errors | count, rate |
Gauge | Current value, can go up or down | Active connections, queue size | min, max, value |
Rate | Percentage of non-zero values | Success rate, cache hit ratio | rate |
Trend | Collects values and computes stats | Response times, processing durations | min, max, avg, med, p(90), p(95), p(99) |
login_duration: ['p(95)<800'] will fail the test if 95% of login durations exceed 800ms. This lets you enforce SLAs on specific business operations, not just generic HTTP stats.Test Lifecycle
K6 scripts have a well-defined lifecycle with three phases: setup(), the default function, and teardown(). Understanding this lifecycle is essential for properly initializing test data, sharing state, and cleaning up after tests.
1import http from 'k6/http';
2import { check, sleep } from 'k6';
3
4export const options = {
5 vus: 10,
6 duration: '30s',
7};
8
9// ----- setup() -----
10// Runs ONCE before the test starts (before any VU code).
11// Use it for: creating test data, authenticating, seeding a database.
12// Whatever this function returns is passed to default() and teardown().
13export function setup() {
14 console.log('--- SETUP: Preparing test data ---');
15
16 // Authenticate and get a token
17 const loginRes = http.post(
18 'https://test-api.k6.io/auth/token/login/',
19 JSON.stringify({
20 username: 'testuser',
21 password: 'superSecure123!',
22 }),
23 { headers: { 'Content-Type': 'application/json' } }
24 );
25
26 const token = loginRes.json('access');
27 console.log('Setup complete. Token acquired.');
28
29 // Return data to be shared with default() and teardown()
30 return { token: token };
31}
32
33// ----- default function -----
34// Runs for EVERY virtual user, in a loop, for the test duration.
35// Receives the data returned by setup() as its argument.
36export default function (data) {
37 const params = {
38 headers: {
39 Authorization: `Bearer ${data.token}`,
40 'Content-Type': 'application/json',
41 },
42 };
43
44 const res = http.get('https://test-api.k6.io/my/crocodiles/', params);
45
46 check(res, {
47 'authenticated request succeeded': (r) => r.status === 200,
48 });
49
50 sleep(1);
51}
52
53// ----- teardown() -----
54// Runs ONCE after all VUs have finished.
55// Use it for: cleaning up test data, logging summary info, sending notifications.
56// Also receives the data returned by setup().
57export function teardown(data) {
58 console.log('--- TEARDOWN: Cleaning up ---');
59 console.log(`Token used during test: ${data.token ? 'Yes' : 'No'}`);
60
61 // Example: delete test data created during the test
62 // http.del('https://test-api.k6.io/my/crocodiles/cleanup/', null, {
63 // headers: { Authorization: `Bearer ${data.token}` },
64 // });
65
66 console.log('Teardown complete.');
67}Lifecycle Phases
- Init code (module-level) — Executes once per VU when the script is loaded. Use this for importing modules and defining options. Do not make HTTP requests here.
- setup() — Runs once, before any VU code. Use it for authentication, creating test data, or verifying preconditions. Returns data that is passed to the default function and teardown.
- default function — Runs for every VU in a loop for the entire test duration. This is where your actual test logic goes.
- teardown() — Runs once, after all VUs finish. Use it for cleanup: deleting test data, sending notifications, or logging summary information.
setup() is serialized to JSON and deserialized in each VU. This means you cannot pass complex objects like functions, classes, or circular references. Stick to simple data types: strings, numbers, arrays, and plain objects.setup() fails (throws an error), the entire test is aborted. Use this to your advantage — check preconditions in setup and fail fast if the environment is not ready, rather than running thousands of failing requests.Full Example: Real-World Scenario Test
Let us bring everything together with a comprehensive, production-ready K6 test. This example uses scenarios to simulate two different types of users simultaneously, custom metrics for business KPIs, groups for organization, the full test lifecycle, and thresholds for automated pass/fail criteria.
1import http from 'k6/http';
2import { check, group, sleep } from 'k6';
3import { Counter, Rate, Trend } from 'k6/metrics';
4
5// ---------------------------------------------------------------------------
6// Custom metrics
7// ---------------------------------------------------------------------------
8const loginDuration = new Trend('login_duration_ms');
9const orderSuccessRate = new Rate('order_success_rate');
10const totalOrders = new Counter('total_orders_placed');
11
12// ---------------------------------------------------------------------------
13// Test configuration
14// ---------------------------------------------------------------------------
15export const options = {
16 scenarios: {
17 // Scenario 1: Browsing users (high volume, read-only)
18 browsers: {
19 executor: 'ramping-vus',
20 startVUs: 0,
21 stages: [
22 { duration: '1m', target: 50 },
23 { duration: '3m', target: 50 },
24 { duration: '1m', target: 0 },
25 ],
26 exec: 'browseProducts',
27 tags: { scenario: 'browse' },
28 },
29
30 // Scenario 2: Purchasing users (lower volume, write operations)
31 buyers: {
32 executor: 'constant-arrival-rate',
33 rate: 10, // 10 iterations per timeUnit
34 timeUnit: '1s', // = 10 requests per second
35 duration: '3m',
36 preAllocatedVUs: 20,
37 maxVUs: 50,
38 exec: 'purchaseFlow',
39 tags: { scenario: 'purchase' },
40 startTime: '30s', // Start 30 seconds after test begins
41 },
42 },
43
44 thresholds: {
45 http_req_duration: ['p(95)<800'],
46 http_req_failed: ['rate<0.05'],
47 login_duration_ms: ['p(95)<1000', 'avg<500'],
48 order_success_rate: ['rate>0.90'],
49 checks: ['rate>0.95'],
50 },
51};
52
53// ---------------------------------------------------------------------------
54// Shared helpers
55// ---------------------------------------------------------------------------
56const BASE_URL = 'https://test-api.k6.io';
57
58function authenticate() {
59 const loginStart = Date.now();
60
61 const res = http.post(
62 `${BASE_URL}/auth/token/login/`,
63 JSON.stringify({
64 username: `testuser${__VU}`,
65 password: 'superSecure123!',
66 }),
67 { headers: { 'Content-Type': 'application/json' } }
68 );
69
70 loginDuration.add(Date.now() - loginStart);
71
72 check(res, {
73 'auth: login succeeded': (r) => r.status === 200,
74 'auth: received token': (r) => r.json('access') !== undefined,
75 });
76
77 return res.json('access');
78}
79
80function getAuthHeaders(token) {
81 return {
82 headers: {
83 Authorization: `Bearer ${token}`,
84 'Content-Type': 'application/json',
85 },
86 };
87}
88
89// ---------------------------------------------------------------------------
90// setup() - Runs once before the test
91// ---------------------------------------------------------------------------
92export function setup() {
93 console.log('Setting up test environment...');
94
95 // Verify the API is healthy before starting
96 const healthCheck = http.get(`${BASE_URL}/public/crocodiles/`);
97 if (healthCheck.status !== 200) {
98 throw new Error(`API health check failed: ${healthCheck.status}`);
99 }
100
101 console.log('API is healthy. Starting test.');
102 return { startTime: Date.now() };
103}
104
105// ---------------------------------------------------------------------------
106// Scenario 1: Browse products (read-only traffic)
107// ---------------------------------------------------------------------------
108export function browseProducts() {
109 group('Browse - List All', function () {
110 const res = http.get(`${BASE_URL}/public/crocodiles/`, {
111 tags: { name: 'ListAllProducts' },
112 });
113
114 check(res, {
115 'browse: list status 200': (r) => r.status === 200,
116 'browse: has items': (r) => r.json().length > 0,
117 });
118 });
119
120 group('Browse - View Details', function () {
121 // Randomly pick a product ID (1-8)
122 const id = Math.floor(Math.random() * 8) + 1;
123
124 const res = http.get(`${BASE_URL}/public/crocodiles/${id}/`, {
125 tags: { name: 'ViewProductDetail' },
126 });
127
128 check(res, {
129 'browse: detail status 200': (r) => r.status === 200,
130 'browse: has name field': (r) => r.json('name') !== undefined,
131 'browse: has valid age': (r) => r.json('age') >= 0,
132 });
133 });
134
135 sleep(Math.random() * 3 + 1); // 1-4 second think time
136}
137
138// ---------------------------------------------------------------------------
139// Scenario 2: Full purchase flow (authenticated, write operations)
140// ---------------------------------------------------------------------------
141export function purchaseFlow() {
142 let token;
143
144 group('Purchase - Login', function () {
145 token = authenticate();
146 });
147
148 if (!token) {
149 orderSuccessRate.add(false);
150 return; // Cannot continue without authentication
151 }
152
153 const authParams = getAuthHeaders(token);
154
155 group('Purchase - Browse', function () {
156 const res = http.get(`${BASE_URL}/my/crocodiles/`, authParams);
157
158 check(res, {
159 'purchase: my items status 200': (r) => r.status === 200,
160 });
161 });
162
163 group('Purchase - Create Order', function () {
164 const orderPayload = JSON.stringify({
165 name: `Croc-${Date.now()}`,
166 sex: Math.random() > 0.5 ? 'M' : 'F',
167 date_of_birth: '2000-05-15',
168 });
169
170 const res = http.post(
171 `${BASE_URL}/my/crocodiles/`,
172 orderPayload,
173 authParams
174 );
175
176 const success = res.status === 201;
177 orderSuccessRate.add(success);
178
179 if (success) {
180 totalOrders.add(1);
181 }
182
183 check(res, {
184 'purchase: create status 201': (r) => r.status === 201,
185 'purchase: has id': (r) => r.json('id') !== undefined,
186 });
187
188 // Clean up: delete the created resource
189 if (success && res.json('id')) {
190 const delRes = http.del(
191 `${BASE_URL}/my/crocodiles/${res.json('id')}/`,
192 null,
193 authParams
194 );
195
196 check(delRes, {
197 'purchase: cleanup succeeded': (r) => r.status === 204,
198 });
199 }
200 });
201
202 sleep(Math.random() * 2 + 1); // 1-3 second think time
203}
204
205// ---------------------------------------------------------------------------
206// teardown() - Runs once after the test
207// ---------------------------------------------------------------------------
208export function teardown(data) {
209 const durationMs = Date.now() - data.startTime;
210 const durationMin = (durationMs / 60000).toFixed(1);
211 console.log(`Test completed in ${durationMin} minutes.`);
212 console.log('Check the k6 summary output above for detailed results.');
213}What This Example Demonstrates
- Scenarios — Two independent workloads run simultaneously:
browsers(read-only traffic with ramping VUs) andbuyers(write operations at a constant arrival rate). This models realistic traffic where most users browse but only some purchase. - Custom metrics —
loginDuration(Trend),orderSuccessRate(Rate), andtotalOrders(Counter) track business-specific KPIs alongside standard HTTP metrics. - Lifecycle hooks —
setup()validates the API is healthy before starting, andteardown()logs test duration. - Groups — Each logical phase of the user journey is wrapped in a group for organized output and targeted thresholds.
- Shared helper functions —
authenticate()andgetAuthHeaders()are reused across scenarios, keeping the code DRY. - Thresholds on custom metrics — The test fails if login duration exceeds targets, if order success rate drops below 90%, or if generic HTTP metrics breach their limits.
- Cleanup logic — The purchase flow deletes test data it creates, preventing test pollution in shared environments.
import/export syntax for organizing complex test suites.