A k6 script can feel like a black box, but it’s actually a sophisticated simulation engine that needs careful tuning to accurately mimic real-world user behavior.

Here’s a k6 script that demonstrates common HTTP request patterns:

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

export const options = {
  stages: [
    { duration: '1m', target: 10 }, // Ramp up to 10 users over 1 minute
    { duration: '3m', target: 10 }, // Stay at 10 users for 3 minutes
    { duration: '1m', target: 0 },  // Ramp down to 0 users over 1 minute
  ],
  thresholds: {
    http_req_failed: '2%', // http errors should be less than 2%
    http_req_duration: '500', // response time should be less than 500ms
  },
};

export default function () {
  // --- GET Request Example ---
  const getResponse = http.get('https://httpbin.org/get');
  check(getResponse, {
    'GET request status is 200': (r) => r.status === 200,
  });
  sleep(1); // Simulate user think time

  // --- POST Request Example ---
  const postData = JSON.stringify({
    key1: 'value1',
    key2: 'value2',
  });
  const postHeaders = {
    'Content-Type': 'application/json',
  };
  const postResponse = http.post('https://httpbin.org/post', postData, postHeaders);
  check(postResponse, {
    'POST request status is 200': (r) => r.status === 200,
    'POST request body contains sent data': (r) => r.json().json.key1 === 'value1',
  });
  sleep(2);

  // --- GET Request with Query Parameters ---
  const queryParams = {
    param1: 'abc',
    param2: '123',
  };
  const getWithParamsResponse = http.get('https://httpbin.org/get', queryParams);
  check(getWithParamsResponse, {
    'GET with params status is 200': (r) => r.status === 200,
    'GET with params contains param1': (r) => r.json().args.param1 === 'abc',
  });
  sleep(1);

  // --- GET Request with Authentication (Basic Auth) ---
  const authResponse = http.get('https://httpbin.org/basic-auth/user/password', {
    headers: {
      'Authorization': 'Basic ' + btoa('user:password'),
    },
  });
  check(authResponse, {
    'Basic Auth GET status is 200': (r) => r.status === 200,
  });
  sleep(1.5);

  // --- POST Request with Authentication (Bearer Token) ---
  const token = 'your_super_secret_token'; // In a real scenario, this would be dynamic
  const postAuthData = JSON.stringify({ message: 'authenticated post' });
  const postAuthHeaders = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  };
  const postAuthResponse = http.post('https://httpbin.org/post', postAuthData, postAuthHeaders);
  check(postAuthResponse, {
    'Bearer Auth POST status is 200': (r) => r.status === 200,
    'Bearer Auth POST contains token': (r) => r.json().headers['Authorization'] === `Bearer ${token}`,
  });
  sleep(2);
}

This script simulates a user interacting with an API. It starts by gradually increasing the number of virtual users to 10 over a minute, maintains that load for three minutes, and then ramps down. Crucially, it defines thresholds to automatically fail the test if more than 2% of requests fail or if any response takes longer than 500ms.

The default function is the core of the k6 test; it defines the actions each virtual user will perform repeatedly. We use http.get() for retrieving data and http.post() for sending data. Notice how we pass JSON.stringify() for the POST body and set the Content-Type header to application/json. For authentication, we demonstrate Basic Auth by manually constructing the Authorization header with a Base64 encoded string and Bearer Token authentication by passing Authorization: Bearer <token>. check() is used to assert expected outcomes, like status codes or content, and sleep() simulates the pauses a real user would take between actions.

The most surprising true thing about k6’s performance metrics is that http_req_duration measures the entire duration of a request, from the moment k6 initiates it to the moment it receives the entire response body. This means it includes DNS lookup, TCP connection, TLS handshake, and the actual time the server takes to process and send the response.

This comprehensive measurement is powerful because it captures all the network overhead and server-side processing. However, it also means that if your network latency is high, or if the server is slow to start sending any data (Time to First Byte - TTFB), http_req_duration will reflect that. To isolate server processing time, you’d often need to combine k6 metrics with server-side logging or use custom metrics within your script to time specific parts of the request lifecycle.

Understanding how k6 measures request duration is key to interpreting your performance test results accurately. The next concept you’ll likely grapple with is how to handle dynamic data, like session tokens or user IDs, that change between requests.

Want structured learning?

Take the full K6 course →