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?

FeatureK6JMeterGatling
Scripting LanguageJavaScript (ES6)XML / GUIScala / Java
Learning CurveLow (JS knowledge)Medium (GUI-based)Medium (Scala DSL)
CI/CD IntegrationNative CLI, easyPossible with pluginsGood (Maven/Gradle)
Resource UsageLightweight (Go-based)Heavy (JVM)Moderate (JVM)
Protocol SupportHTTP, WebSocket, gRPCHTTP, JDBC, JMS, LDAP, FTPHTTP, WebSocket, JMS
Real-time ResultsCLI output + streaming to dashboardsGUI + listenersHTML report after run
Browser TestingK6 browser module (Chromium)Not nativelyNot natively
Cloud OptionGrafana Cloud K6BlazeMeterGatling Enterprise
Key TakeawayK6 excels when your team already writes JavaScript or TypeScript. Its code-first approach, lightweight execution, and native CI/CD friendliness make it the go-to choice for modern dev teams who want performance testing as part of their daily workflow.

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.

terminalbash
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/releases

Verify Installation

After installation, verify K6 is working by checking its version:

terminalbash
1# Check K6 version
2k6 version
3
4# Expected output:
5# k6 v0.49.0 (go1.22.0, linux/amd64)
TipFor CI/CD pipelines, Docker is often the easiest approach. The official grafana/k6 image contains everything needed. Mount your test scripts as a volume: docker run --rm -v $(pwd):/scripts grafana/k6 run /scripts/test.js

Writing 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.

script.jsjavascript
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.

terminalbash
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.js

Understanding 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.
Virtual Users vs. RequestsA virtual user (VU) is a concurrent user executing your script. Each VU runs the default function in a loop. If you have 10 VUs and each iteration takes 2 seconds (including sleep), you will generate roughly 5 requests per second per VU, or about 50 requests per second total.

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

get-example.jsjavascript
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

post-example.jsjavascript
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

put-delete-example.jsjavascript
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}
TipAlways use 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.
WarningK6 uses its own JavaScript runtime (not Node.js), so you cannot use 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.

checks-example.jsjavascript
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.

thresholds-example.jsjavascript
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

ExpressionMeaning
p(95)<50095th percentile must be under 500ms
p(99)<150099th percentile must be under 1500ms
avg<200Average value must be under 200ms
max<3000Maximum value must be under 3000ms
med<300Median (50th percentile) under 300ms
rate<0.01Rate metric must be under 1%
rate>0.95Rate metric must be above 95%
count<100Counter must be under 100
Checks vs. ThresholdsChecks are per-request validations that track pass rates. Thresholds are test-level pass/fail criteria. Use checks to validate responses, then use thresholds on the 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.

smoke-test.jsjavascript
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.

load-test.jsjavascript
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.

stress-test.jsjavascript
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.

spike-test.jsjavascript
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.

soak-test.jsjavascript
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 TypeVUsDurationGoal
Smoke11 minuteVerify basic functionality
Load50-10015-30 minutesValidate normal traffic SLAs
Stress100-400+30-60 minutesFind the breaking point
Spike500+ (burst)10-15 minutesTest sudden traffic surges
Soak50-1004-12 hoursFind memory leaks and degradation
TipAlways run tests in this order: Smoke first, then Load, then Stress, then Spike, and finally Soak. Each test builds confidence before moving to the next level. If an earlier test fails, fix those issues before proceeding.

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.

custom-metrics.jsjavascript
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

TypeDescriptionUse CaseAggregations
CounterCumulative sum, only goes upTotal orders placed, total errorscount, rate
GaugeCurrent value, can go up or downActive connections, queue sizemin, max, value
RatePercentage of non-zero valuesSuccess rate, cache hit ratiorate
TrendCollects values and computes statsResponse times, processing durationsmin, max, avg, med, p(90), p(95), p(99)
Custom Metrics in ThresholdsCustom metrics work exactly like built-in metrics in thresholds. For example, 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.

lifecycle-example.jsjavascript
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.
WarningThe data returned by 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.
TipIf 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.

Groups and Tags

Groups let you organize your test logic into named sections, similar to test suites. Tags let you add custom key-value metadata to requests and metrics, enabling granular filtering and targeted thresholds.

groups-tags-example.jsjavascript
1import http from 'k6/http';
2import { check, group, sleep } from 'k6';
3
4export const options = {
5  vus: 5,
6  duration: '30s',
7  thresholds: {
8    // Threshold on a specific group's requests using tags
9    'http_req_duration{group:::Authentication}': ['p(95)<600'],
10    'http_req_duration{group:::Browse Products}': ['p(95)<400'],
11  },
12};
13
14export default function () {
15  // Groups organize your test logic into named sections.
16  // They appear in the output summary and can be used for targeted thresholds.
17
18  group('Authentication', function () {
19    const loginRes = http.post(
20      'https://test-api.k6.io/auth/token/login/',
21      JSON.stringify({
22        username: 'testuser',
23        password: 'superSecure123!',
24      }),
25      {
26        headers: { 'Content-Type': 'application/json' },
27        tags: { name: 'Login' },  // Custom tag for this request
28      }
29    );
30
31    check(loginRes, {
32      'login status is 200': (r) => r.status === 200,
33    });
34  });
35
36  group('Browse Products', function () {
37    // Tags let you filter and organize metrics.
38    // You can add custom tags to any request.
39    const listRes = http.get('https://test-api.k6.io/public/crocodiles/', {
40      tags: { name: 'ListProducts', page: 'catalog' },
41    });
42
43    check(listRes, {
44      'list status is 200': (r) => r.status === 200,
45    });
46
47    // Nested group
48    group('View Product Detail', function () {
49      const detailRes = http.get('https://test-api.k6.io/public/crocodiles/1/', {
50        tags: { name: 'ProductDetail', page: 'detail' },
51      });
52
53      check(detailRes, {
54        'detail status is 200': (r) => r.status === 200,
55        'has product name': (r) => r.json('name') !== undefined,
56      });
57    });
58  });
59
60  sleep(1);
61}

When to Use Groups vs. Tags

  • Groups — Use to organize logical sections of a user journey (e.g., "Login", "Browse", "Checkout"). Groups appear in the output summary as a hierarchy and automatically tag all requests inside them.
  • Tags — Use to add metadata to individual requests for filtering in output and thresholds. Tags are more granular than groups and can be applied to any request independently.
Groups in ThresholdsK6 automatically adds a group tag to all requests made inside a group. The format uses :: as a separator: 'http_req_duration{group:::Authentication}': ['p(95)<600']. This lets you set different performance criteria for different parts of the user journey.

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.

full-scenario-test.jsjavascript
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) and buyers (write operations at a constant arrival rate). This models realistic traffic where most users browse but only some purchase.
  • Custom metricsloginDuration (Trend), orderSuccessRate (Rate), and totalOrders (Counter) track business-specific KPIs alongside standard HTTP metrics.
  • Lifecycle hookssetup() validates the API is healthy before starting, and teardown() logs test duration.
  • Groups — Each logical phase of the user journey is wrapped in a group for organized output and targeted thresholds.
  • Shared helper functionsauthenticate() and getAuthHeaders() 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.
TipFor larger projects, split your test into multiple files using ES6 module imports. Keep helpers, scenarios, and configuration in separate files. K6 supports standard import/export syntax for organizing complex test suites.
Next StepsNow that you have the fundamentals of K6, explore these advanced topics: browser-level testing with the K6 browser module, distributed testing with Grafana Cloud K6, streaming results to Grafana/InfluxDB/Prometheus, writing custom K6 extensions in Go, and integrating K6 into your CI/CD pipeline as an automated quality gate.