Introduction

Grafana K6 is a highly efficient load testing tool built with JavaScript (with a Golang core) that maximizes the load capabilities of a single machine. According to the official documentation, a single K6 process can effectively utilize all CPU cores, and under ideal conditions, it can simulate 30,000–40,000 virtual users (VUs). This is typically sufficient to handle 100,000–300,000 requests per second (RPS), translating to 6–12 million requests per minute. It enables more efficient load testing without requiring additional hardware resources.

Installation

The official installation documentation provides complete guidance. Below are instructions for some common operating systems:

MacOS (via Homebrew):

brew install k6

Windows (via Chocolatey):

choco install k6

Docker

docker pull grafana/k6

Ubuntu

sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6

Getting Started with K6

Here’s an example using Mac or Linux:

Create a new K6 script using the following command. This generates a sample script.js with default settings to simulate 10 virtual users (VUs) for 30 seconds, accessing a specified endpoint at one-second intervals:

k6 new 

The generated script.js looks like this:

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  // A number specifying the number of VUs to run concurrently.
  vus: 10,
  // A string specifying the total duration of the test run.
  duration: '30s',
};
// The function that defines VU logic.
//
//
export default function() {
  http.get('https://test.k6.io');
  sleep(1);
}

Run the script with:

k6 run script.js

You’ll see output like this during execution:

> k6 run script.js

         /\      Grafana   /‾‾/
    /\  /  \     |\  __   /  /
   /  \/    \    | |/ /  /   ‾‾\
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/

     execution: local
        script: script.js
        output: -

     scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
              * default: 10 looping VUs for 30s (gracefulStop: 30s)


running (0m18.1s), 10/10 VUs, 140 complete and 0 interrupted iterations
default   [=====================>----------------] 10 VUs  18.1s/30s

When the test completes, K6 outputs the results:


         /\      Grafana   /‾‾/
    /\  /  \     |\  __   /  /
   /  \/    \    | |/ /  /   ‾‾\
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/

     execution: local
        script: script.js
        output: -

     scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
              * default: 10 looping VUs for 30s (gracefulStop: 30s)


     data_received..................: 2.7 MB 84 kB/s
     data_sent......................: 26 kB  812 B/s
     http_req_blocked...............: avg=19.77ms  min=3µs      med=10µs     max=457.38ms p(90)=21µs     p(95)=63.99µs
     http_req_connecting............: avg=9.22ms   min=0s       med=0s       max=219.36ms p(90)=0s       p(95)=0s
     http_req_duration..............: avg=326.52ms min=197.05ms med=219.61ms max=1.72s    p(90)=596.47ms p(95)=742.65ms
       { expected_response:true }...: avg=326.52ms min=197.05ms med=219.61ms max=1.72s    p(90)=596.47ms p(95)=742.65ms
     http_req_failed................: 0.00%  0 out of 229
     http_req_receiving.............: avg=26.83ms  min=33µs     med=120µs    max=220.24ms p(90)=193.91ms p(95)=196.41ms
     http_req_sending...............: avg=32.92µs  min=6µs      med=28µs     max=565µs    p(90)=47.2µs   p(95)=56.19µs
     http_req_tls_handshaking.......: avg=9.44ms   min=0s       med=0s       max=220.48ms p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=299.66ms min=196.85ms med=212.8ms  max=1.72s    p(90)=524.54ms p(95)=638.33ms
     http_reqs......................: 229    7.184528/s
     iteration_duration.............: avg=1.34s    min=1.19s    med=1.22s    max=2.72s    p(90)=1.65s    p(95)=1.74s
     iterations.....................: 229    7.184528/s
     vus............................: 2      min=2        max=10
     vus_max........................: 10     min=10       max=10


running (0m31.9s), 00/10 VUs, 229 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs  30s

Interpreting Test Results

Execution: local, indicating the test was executed locally, without using a distributed mode. Script: script.js, specifying that the test script executed is script.js. Output: -, no output file specified, with results directly output to the terminal.

scenarios:

  • 1 scenario: Only one test scenario is defined.
  • 10 max VUs: Up to 10 virtual users (VUs) were activated.
  • 1m0s max duration: The maximum test duration is 60 seconds, including a “graceful stop” period after the test ends.
  • 30s duration: The test actually ran for 30 seconds, with each VU repeatedly executing requests.

data_received: 2.7 MB Indicates that 2.7 MB of data was received from the server during the test (average speed: 84 kB/s).

data_sent: 26 kB Indicates that 26 kB of data was sent to the server during the test (average speed: 812 B/s).

http_req_blocked: Time spent with requests being blocked (e.g., waiting for connection establishment). Average (avg): 19.77ms Maximum (max): 457.38ms P90 (90% of requests below): 21µs P95 (95% of requests below): 63.99µs

http_req_connecting: Time spent establishing connections with the server. Average: 9.22ms Maximum: 219.36ms

http_req_duration: Total duration of HTTP requests (from sending to receiving the full response). Average: 326.52ms Minimum: 197.05ms Maximum: 1.72s Median (med): 219.61ms P90: 596.47ms P95: 742.65ms

http_req_waiting: Time spent waiting for the server to process the request (from sending to the first byte of response). Average: 299.66ms Maximum: 1.72s P90: 524.54ms

http_req_failed: Failure rate of requests. 0.00% (no failed requests).

http_req_receiving: Time spent receiving response data. Average: 26.83ms Maximum: 220.24ms

http_req_sending: Time spent sending request data. Average: 32.92µs

http_req_tls_handshaking: Time spent on TLS handshake. Average: 9.44ms Maximum: 220.48ms

http_reqs: 229 HTTP requests were completed during the test, with an average of 7.18 requests per second (RPS).

iteration_duration: Total duration of each iteration (including request, processing, and response). Average: 1.34s Maximum: 2.72s

iterations: A total of 229 iterations were completed, with an average of 7.18 iterations per second. vus: Active virtual users during the test ranged from a minimum of 2 to a maximum of 10. vus_max: The maximum number of virtual users during the test was 10.

Common Metrics

Typically, there are several key metrics prioritized during evaluation: First, the failure rate and overall RPS performance are observed through the following indicators:

http_req_failed: Request failure rate (only HTTP status code 200 is considered successful by default). http_reqs: Total number of requests sent and RPS (requests per second). When results fall below expectations, the following request cycle metrics are reviewed:

http_req_sending: Time spent sending the request. http_req_receiving: Time spent receiving the response. http_req_waiting: Time spent waiting for the response. http_req_duration: Total request duration (http_req_sending + http_req_waiting + http_req_receiving).

Common Parameters

vus: Number of virtual users (minimum: 1). duration: Duration of the test. interactions: Number of test executions. stages: Allows specifying various phases to simulate different test scenarios (e.g., ramp-up testing as shown below). rps: Maximum number of requests per second that VUs can send.

Test Method Overview

The K6 Testing Guides describe the following test methods:

  • Smoke testing: Validates if the system functions correctly under minimal load after changes.
  • Average-load testing: Also known as “day-in-life” or “volume testing,” evaluates system performance under normal online conditions.
  • Stress testing: Assesses system capacity when subjected to load beyond average online usage.
  • Soak testing: Evaluates system reliability and performance under average usage over an extended time.
  • Spike testing: Verifies system behavior and survival under sudden, short bursts of high activity.
  • Breakpoint testing: Gradually increases load to determine the system’s capacity limits.
Type VUs/Throughput Duration When?
Smoke Low Seconds to minutes After code changes, validate function logic and baseline metrics.
Load Avg. Production 5–60 minutes Ensure consistent performance under average usage.
Stress Above Avg. 5–60 minutes Assess performance under higher-than-average loads.
Soak Avg. Production ≥1 hour Validate long-term system stability.
Spike Extremely High Minutes Ensure the system can handle sudden traffic spikes.
Breakpoint Increasing until failure As needed Identify system limits.

Checks

K6 provides an assertion-like Check feature. Unlike standard test frameworks, K6 continues testing even when a check fails, avoiding an abort.

Example:

import { check } from 'k6';
import http from 'k6/http';

export default function () {
  const res = http.get('http://test.k6.io/');
  check(res, {
    'is status 200': (r) => r.status === 200,
  });
}

Result output reflects assertion outcomes:

$ k6 run script.js

  ...
    ✓ is status 200

  ...
  checks.........................: 100.00% ✓ 10
  data_received..................: 11 kB   12 kB/s

Thresholds

Thresholds enable custom limits to improve test efficiency by focusing on success criteria. Example scenarios:

  • Less than 1% of requests return errors.
  • 95% of requests have response times below 200ms.
  • 99% of requests have response times below 400ms.
  • A specific endpoint always responds within 300ms.

Example:

import http from 'k6/http';

export const options = {
  thresholds: {
    http_req_failed: ['rate<0.01'], // http errors should be less than 1%
    http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms
  },
};

export default function () {
  http.get('https://test-api.k6.io/public/crocodiles/1/');
}

Output example:

   ✓ http_req_duration..............: avg=151.06ms min=151.06ms med=151.06ms max=151.06ms p(90)=151.06ms p(95)=151.06ms
       { expected_response:true }...: avg=151.06ms min=151.06ms med=151.06ms max=151.06ms p(90)=151.06ms p(95)=151.06ms
   ✓ http_req_failed................: 0.00%  ✓ 01

Common Testing Scripts

(1) Simple Testing

Scenario: Maintain 3 users over a specified time period (usually fluctuating around ±2 users).

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 3, // Key for Smoke test. Keep it at 2, 3, max 5 VUs
  duration: '1m', // This can be shorter or just a few iterations
};

export default () => {
  const urlRes = http.get('https://test-api.k6.io');
  sleep(1);
  // MORE STEPS
  // Here you can have more steps or complex script
  // Step1
  // Step2
  // etc.
};

(2) Ramp-Up Testing

Unless performing spike testing or breakpoint testing, a slower ramp-up approach is typically recommended, gradually increasing the load.

Scenario: Incrementally ramp-up users from 1 to 200 over a specific duration, then maintain user traffic.

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  // Key configurations for Stress in this section
  stages: [
    { duration: '10m', target: 200 }, // traffic ramp-up from 1 to a higher 200 users over 10 minutes.
    { duration: '30m', target: 200 }, // stay at higher 200 users for 30 minutes
    { duration: '5m', target: 0 }, // ramp-down to 0 users
  ],
};

export default () => {
  const urlRes = http.get('https://test-api.k6.io');
  sleep(1);
  // MORE STEPS
  // Here you can have more steps or complex script
  // Step1
  // Step2
  // etc.
};

Scenario: Use a rate-based (RPS) approach to achieve specific request-per-second goals. (Reference)

import http from 'k6/http';

export const options = {
  discardResponseBodies: true,

  scenarios: {
    contacts: {
      executor: 'ramping-arrival-rate',

      // Start iterations per `timeUnit`
      startRate: 200,

      // Start `startRate` iterations per minute
      timeUnit: '1m',

      // Pre-allocate necessary VUs.
      preAllocatedVUs: 50,

      stages: [
        { target: 100, duration: '1m' },
        { target: 150, duration: '1m' },
        { target: 200, duration: '10m' },
        { target: 0, duration: '2m' },
      ],
    },
  },
};

export default function () {
  http.get('https://test.k6.io/contacts.php');
}

(3) Load Testing

Scenario: Simulate a sudden spike of traffic during spike testing with 2000 users within 2 minutes.

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  // Key configurations for spike in this section
  stages: [
    { duration: '2m', target: 2000 }, // fast ramp-up to a high point
    // No plateau
    { duration: '1m', target: 0 }, // quick ramp-down to 0 users
  ],
};

export default () => {
  const urlRes = http.get('https://test-api.k6.io');
  sleep(1);
  // MORE STEPS
  // Add only the processes that will be on high demand
  // Step1
  // Step2
  // etc.
};

Scenario: Gradually increase the load for breakpoint testing to determine the system’s error threshold.

Here, executor: ‘ramping-arrival-rate’ is used to continuously increase the load even when the system slows down.

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  // Key configurations for breakpoint in this section
  executor: 'ramping-arrival-rate', //Assure load increase if the system slows
  stages: [
    { duration: '2h', target: 20000 }, // just slowly ramp-up to a HUGE load
  ],
};

export default () => {
  const urlRes = http.get('https://test-api.k6.io');
  sleep(1);
  // MORE STEPS
  // Here you can have more steps or complex script
  // Step1
  // Step2
  // etc.
};

Designing for Real-World Use Cases

In practical scenarios, testing often needs to simulate a full user journey, such as navigating a homepage, viewing product details, and making purchases.

Example: Use group to organize test steps into nested structures to better reflect real user behavior.

import http from 'k6/http';
import exec from 'k6/execution';
import { group } from 'k6';

export const options = {
  thresholds: {
    'http_reqs{container_group:main}': ['count==3'],
    'http_req_duration{container_group:main}': ['max<1000'],
  },
};

export default function () {
  exec.vu.tags.containerGroup = 'main';

  group('main', function () {
    http.get('https://test.k6.io');
    group('sub', function () {
      http.get('https://httpbin.test.k6.io/anything');
    });
    http.get('https://test-api.k6.io');
  });

  group('visit product listing page', function () {
    // ...
  });
  group('add several products to the shopping cart', function () {
    // ...
  });
  group('visit login page', function () {
    // ...
  });
  group('authenticate', function () {
    // ...
  });
  group('checkout process', function () {
    // ...
  });

  delete exec.vu.tags.containerGroup;

  http.get('https://httpbin.test.k6.io/delay/3');
}

It is not recommended for each Group to be responsible for a single request, as this would defeat the purpose of using a Group.


import { group, check } from 'k6';
import http from 'k6/http';

const id = 5;

// reconsider this type of code
group('get post', function () {
  http.get(`http://example.com/posts/${id}`);
});
group('list posts', function () {
  const res = http.get(`http://example.com/posts`);
  check(res, {
    'is status 200': (r) => r.status === 200,
  });
});

Alternatively, design for different scenarios.

import http from 'k6/http';
import { fail } from 'k6';

export const options = {
  discardResponseBodies: true,
  scenarios: {
    contacts: {
      executor: 'constant-vus',
      exec: 'contacts',
      vus: 50,
      duration: '30s',
      tags: { my_custom_tag: 'contacts' },
      env: { MYVAR: 'contacts' },
    },
    news: {
      executor: 'per-vu-iterations',
      exec: 'news',
      vus: 50,
      iterations: 100,
      startTime: '30s',
      maxDuration: '1m',
      tags: { my_custom_tag: 'news' },
      env: { MYVAR: 'news' },
    },
  },
};

export function contacts() {
  if (__ENV.MYVAR != 'contacts') fail();
  http.get('https://test.k6.io/contacts.php');
}

export function news() {
  if (__ENV.MYVAR != 'news') fail();
  http.get('https://test.k6.io/news.php');
}

For more examples tailored to specific use cases (e.g., OAuth, WebSocket, HTML parsing), refer to the official K6 documentation.