k6

k6 is a code-first load testing tool written in Go, scripted in JavaScript/TypeScript — VU model, threshold-based pass/fail, native WebSocket support, and first-class CI integration via exit codes.

k6 is an open-source load testing tool built by Grafana Labs. Tests are written in JavaScript (TypeScript supported via transpilation), run by a Go runtime, and produce threshold-based pass/fail outcomes suitable for CI integration. The Go VU model is significantly lighter than JMeter's JVM-thread-per-VU model, allowing higher concurrency on the same hardware.

See technical-qa/jmeter for the comparison with Apache JMeter and test-automation/performance-testing for the broader performance test taxonomy.


Core Concepts

Virtual Users (VUs) — concurrent simulated users. Each VU runs the default function in a loop for the duration of the test. k6 uses Go goroutines per VU; a single machine can sustain thousands of VUs without the heap pressure that JMeter's thread-per-VU model creates.

Thresholds — pass/fail criteria expressed in the test script. CI exits with code 0 when all thresholds pass, non-zero when any fail. This makes k6 natively usable as a CI gate without a separate threshold-checking script.

Checks — assertions within test code. A check failure increments a counter but does not abort the test. Checks feed into the checks rate metric, which can be used in a threshold.


Basic Script

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

export const options = {
  vus: 50,
  duration: '5m',
  thresholds: {
    http_req_duration: ['p(95)<500'],   // 95th percentile under 500ms
    http_req_failed: ['rate<0.01'],      // error rate under 1%
    checks: ['rate>0.99'],               // 99% of checks must pass
  },
};

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

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 200ms': (r) => r.timings.duration < 200,
    'has products': (r) => JSON.parse(r.body).length > 0,
  });

  sleep(1);
}

Ramp Profiles with Stages

export const options = {
  stages: [
    { duration: '2m', target: 10 },   // ramp up to 10 VUs
    { duration: '5m', target: 100 },  // ramp to 100 VUs
    { duration: '10m', target: 100 }, // hold 100 VUs (steady state)
    { duration: '2m', target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<1000', 'p(99)<2000'],
  },
};

POST Requests with JSON Body

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

export default function () {
  const payload = JSON.stringify({ productId: 'abc123', quantity: 1 });
  const params = { headers: { 'Content-Type': 'application/json' } };

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

  check(res, {
    'order created': (r) => r.status === 201,
    'order id present': (r) => JSON.parse(r.body).orderId !== undefined,
  });
}

WebSocket Support

k6 supports WebSocket testing via k6/experimental/websockets (stable in k6 v0.43+):

import { WebSocket } from 'k6/experimental/websockets';
import { check } from 'k6';

export default function () {
  const ws = new WebSocket('wss://api.example.com/ws');

  ws.onopen = () => {
    ws.send(JSON.stringify({ type: 'subscribe', channel: 'orders' }));
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    check(msg, { 'received order event': (m) => m.type === 'order_created' });
    ws.close();
  };
}

For sustained WebSocket load tests, use k6/ws (the older API) with a controlled session.Duration so sessions close after a defined period rather than running until the test ends.


CI Integration (GitHub Actions)

- name: Run load test
  run: k6 run --vus 20 --duration 60s tests/load/api.js

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

k6 exits with code 1 if any threshold is breached. GitHub Actions interprets this as a failed step, blocking the pipeline without additional tooling.


k6 Cloud

Grafana's hosted execution environment for k6. Provides: geographically distributed load generation, real-time dashboards, historical test comparison, and team collaboration. Use when peak VU count exceeds what a single CI runner can generate (~500-1000 VUs depending on think time and response size).

k6 cloud tests/load/api.js

Requires a K6_CLOUD_TOKEN environment variable and projectID set in the script options.


k6 vs JMeter

Dimensionk6JMeter
Test definitionJavaScript/TypeScriptXML + GUI
VU modelGo goroutine (lightweight)JVM thread (heavier)
CI integrationNative (threshold exit codes)Requires wrapper script
WebSocketNative (experimental/websockets)Plugin required
Non-HTTP protocolsHTTP, WS, gRPC via extensionsJDBC, JMS, LDAP, FTP, TCP natively
Existing legacy suitesNo .jmx compatFull .jmx ecosystem

Choose k6 for greenfield, developer-led performance testing. Choose JMeter when the client has existing .jmx files or needs non-HTTP protocol coverage. See technical-qa/jmeter for the full decision framework.


Connections

Open Questions

  • What's the practical VU count ceiling on a single k6 runner before needing k6 Cloud or distributed k6 instances?
  • How do you model realistic user think time in k6 without introducing so much sleep that the test runs for hours?
  • When does k6 + Grafana Cloud become more cost-effective than running k6 locally and storing metrics in InfluxDB?