TL;DR

  • k6 is a modern, developer-friendly load testing tool — write tests in JavaScript
  • Install with brew/apt/docker, write scripts, run from CLI
  • Define thresholds for pass/fail criteria: response time, error rate
  • Built-in metrics: http_req_duration, http_reqs, iterations, vus
  • Integrates with CI/CD, Grafana Cloud, InfluxDB for dashboards

Best for: Developers, DevOps, teams wanting code-based performance tests Skip if: You need GUI-based test design or exotic protocols (use JMeter) Reading time: 15 minutes

Your team wants performance tests in CI/CD. JMeter XML files are hard to review in PRs. Tests need to run headlessly in containers.

k6 solves this. Tests are JavaScript code — readable, versionable, reviewable. Runs from CLI, integrates with everything, reports in real-time.

This tutorial teaches k6 from installation to CI/CD integration.

What is k6?

k6 is an open-source load testing tool built for modern development workflows. Tests are JavaScript, execution is Go (fast), and results integrate with observability tools.

Why k6:

  • JavaScript tests — use a real language, not XML
  • Developer experience — great CLI, clear output
  • High performance — single machine handles thousands of VUs
  • CI/CD native — runs in containers, outputs machine-readable results
  • Extensible — JavaScript APIs, extensions, protocols

Installation

# macOS
brew install k6

# Ubuntu/Debian
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 update && sudo apt install k6

# Docker
docker run --rm -i grafana/k6 run - <script.js

# Windows
choco install k6

Verify installation:

k6 version

Your First k6 Test

Create script.js:

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

export const options = {
  vus: 10,           // 10 virtual users
  duration: '30s',   // run for 30 seconds
};

export default function () {
  const res = http.get('https://test.k6.io');

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}

Run it:

k6 run script.js

k6 Concepts

Virtual Users (VUs)

VUs are parallel executors running your test function.

export const options = {
  vus: 100,        // concurrent users
  duration: '5m',  // test duration
};

Stages (Ramp-up/down)

Gradually increase and decrease load.

export const options = {
  stages: [
    { duration: '2m', target: 100 },  // ramp to 100 users
    { duration: '5m', target: 100 },  // stay at 100
    { duration: '2m', target: 200 },  // ramp to 200
    { duration: '5m', target: 200 },  // stay at 200
    { duration: '2m', target: 0 },    // ramp down
  ],
};

Thresholds

Define pass/fail criteria.

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<500'],     // 95% of requests < 500ms
    http_req_failed: ['rate<0.01'],       // error rate < 1%
    checks: ['rate>0.99'],                // 99% of checks pass
    http_reqs: ['rate>100'],              // at least 100 RPS
  },
};

HTTP Requests

GET Request

import http from 'k6/http';

export default function () {
  // Simple GET
  const res = http.get('https://api.example.com/users');

  // GET with parameters
  const res2 = http.get('https://api.example.com/users?page=1&limit=10');

  // GET with headers
  const res3 = http.get('https://api.example.com/users', {
    headers: {
      'Authorization': 'Bearer token123',
      'Accept': 'application/json',
    },
  });
}

POST Request

import http from 'k6/http';

export default function () {
  const payload = JSON.stringify({
    name: 'John Doe',
    email: 'john@example.com',
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const res = http.post('https://api.example.com/users', payload, params);
}

Other Methods

// PUT
http.put(url, payload, params);

// PATCH
http.patch(url, payload, params);

// DELETE
http.del(url, params);

// Batch requests (parallel)
const responses = http.batch([
  ['GET', 'https://api.example.com/users'],
  ['GET', 'https://api.example.com/products'],
  ['GET', 'https://api.example.com/orders'],
]);

Checks and Assertions

import { check } from 'k6';

export default function () {
  const res = http.get('https://api.example.com/users');

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response has users': (r) => r.json().length > 0,
    'response time OK': (r) => r.timings.duration < 500,
    'content-type is JSON': (r) =>
      r.headers['Content-Type'].includes('application/json'),
  });
}

Test Data

Environment Variables

// script.js
const baseUrl = __ENV.BASE_URL || 'https://test.k6.io';

export default function () {
  http.get(`${baseUrl}/api/users`);
}
k6 run -e BASE_URL=https://staging.example.com script.js

JSON Data File

// data.json
// [{"username": "user1", "password": "pass1"}, ...]

import { SharedArray } from 'k6/data';

const users = new SharedArray('users', function () {
  return JSON.parse(open('./data.json'));
});

export default function () {
  const user = users[Math.floor(Math.random() * users.length)];
  // use user.username, user.password
}

CSV Data

import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
import { SharedArray } from 'k6/data';

const csvData = new SharedArray('users', function () {
  return papaparse.parse(open('./users.csv'), { header: true }).data;
});

Scenarios

Advanced execution patterns.

export const options = {
  scenarios: {
    // Constant load
    constant_load: {
      executor: 'constant-vus',
      vus: 50,
      duration: '5m',
    },

    // Ramping load
    ramping_load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 100 },
        { duration: '5m', target: 100 },
        { duration: '2m', target: 0 },
      ],
    },

    // Constant arrival rate
    constant_rate: {
      executor: 'constant-arrival-rate',
      rate: 100,              // 100 iterations per timeUnit
      timeUnit: '1s',         // per second
      duration: '5m',
      preAllocatedVUs: 50,
    },

    // Spike test
    spike: {
      executor: 'ramping-arrival-rate',
      startRate: 10,
      timeUnit: '1s',
      stages: [
        { duration: '10s', target: 10 },
        { duration: '1m', target: 1000 },  // spike!
        { duration: '10s', target: 10 },
      ],
      preAllocatedVUs: 500,
    },
  },
};

Metrics and Output

Built-in Metrics

MetricDescription
http_reqsTotal HTTP requests
http_req_durationRequest duration
http_req_failedFailed request rate
iterationsCompleted iterations
vusCurrent virtual users
data_receivedData received
data_sentData sent

Custom Metrics

import { Counter, Trend, Rate, Gauge } from 'k6/metrics';

const orderCounter = new Counter('orders_created');
const orderDuration = new Trend('order_duration');
const orderSuccess = new Rate('order_success_rate');
const activeOrders = new Gauge('active_orders');

export default function () {
  const start = Date.now();

  const res = http.post('https://api.example.com/orders', orderPayload);

  orderCounter.add(1);
  orderDuration.add(Date.now() - start);
  orderSuccess.add(res.status === 201);
  activeOrders.add(10);
}

JSON Output

k6 run --out json=results.json script.js

InfluxDB + Grafana

k6 run --out influxdb=http://localhost:8086/k6 script.js

CI/CD Integration

GitHub Actions

name: Load Test

on:
  push:
    branches: [main]

jobs:
  k6-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run k6 test
        uses: grafana/k6-action@v0.3.1
        with:
          filename: tests/load-test.js
          flags: --out json=results.json

      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: k6-results
          path: results.json

Docker

FROM grafana/k6

COPY ./tests /tests

ENTRYPOINT ["k6", "run", "/tests/script.js"]
docker build -t my-k6-tests .
docker run my-k6-tests

k6 with AI Assistance

AI tools can help write and optimize k6 tests.

What AI does well:

  • Generate test scripts from API specs
  • Create realistic test data
  • Suggest threshold values
  • Explain metrics and results

What still needs humans:

  • Understanding system architecture
  • Setting realistic load targets
  • Interpreting results in context
  • Debugging failures

FAQ

What is k6?

k6 is a modern, open-source load testing tool designed for developers. You write tests in JavaScript, run them from the command line, and get detailed performance metrics. Built by Grafana Labs, k6 focuses on developer experience, CI/CD integration, and high performance — a single machine can simulate thousands of virtual users.

Is k6 free?

Yes, k6 open-source (k6 OSS) is completely free. Grafana Cloud k6 offers hosted test execution, cloud storage, and additional features with both free and paid tiers. For most use cases, the open-source version provides everything you need.

k6 vs JMeter — which is better?

k6 is better for developers and CI/CD workflows — tests are code (JavaScript), execution is fast, and it integrates well with modern tooling. JMeter is better for GUI-based test design and supports more protocols out of the box. k6 is generally easier to learn and maintain for teams familiar with JavaScript.

Can k6 test browser interactions?

Yes, k6 has an experimental browser module that enables real browser testing using Chromium. However, k6’s strength is protocol-level testing (HTTP, WebSocket, gRPC) which is much more efficient for load testing. Use the browser module when you need to test JavaScript-rendered pages or complex browser interactions.

Official Resources

See Also