Residential Proxies Web Scraping API Turn Sites Into AI Data Automate 3000+ Apps Learn Python Automation Pay As You Go Proxies
Residential Proxies Web Scraping API
Pay As You Go Proxies 10 Free Proxies Antidetect Browser No Code Browser Bots Web Data For AI Agents Hire Scraper Builders

k6 Load Testing: Write Your First Test in JavaScript

Updated August 2026
k6 is an open source load testing tool from Grafana Labs that runs JavaScript test scripts on a fast Go engine, letting one machine simulate thousands of concurrent users. You write a function describing what one virtual user does, define the load shape in an options object, and k6 handles the concurrency, measurement, and reporting. This tutorial goes from install to a CI-ready test with realistic stages, response checks, and pass/fail thresholds.

Step 1: Install k6

k6 ships as a single binary. On macOS, brew install k6. On Debian and Ubuntu, add Grafana's package repository and apt install k6, or grab the .deb from the releases page. On Windows, winget install k6 --source winget or choco install k6. Docker users can skip installation entirely with docker run --rm -i grafana/k6 run - <script.js. Verify with k6 version.

One conceptual note before writing code: k6 scripts look like modern JavaScript, with ES module imports and familiar syntax, but they run inside k6's Go runtime, not Node.js. There is no npm, no filesystem module, no DOM. The k6 standard library covers HTTP, WebSockets, gRPC, metrics, and utilities, and for most load tests it is all you need.

Step 2: Write a Basic Test

A minimal k6 script is a default function that one virtual user executes repeatedly:

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

export default function () {
  http.get('https://staging.example.com/');
  sleep(1);
}

Run it with k6 run script.js and k6 executes the function with one virtual user for one iteration, printing a metrics summary. Add flags for a quick smoke of concurrency: k6 run --vus 10 --duration 30s script.js runs 10 virtual users looping for 30 seconds.

The sleep(1) is think time, the pause a real user takes between actions. Without it, each virtual user hammers requests as fast as responses return, and your "10 users" simulate the load of a hundred. Real tests randomize it, sleep(Math.random() * 3 + 2) for 2 to 5 seconds, matching how humans actually pace through pages.

Step 3: Shape the Load with Stages

Real load profiles ramp up, hold, and ramp down, and k6 expresses this in the options object:

export const options = {
  stages: [
    { duration: '3m', target: 100 },
    { duration: '15m', target: 100 },
    { duration: '2m', target: 0 },
  ],
};

This climbs to 100 virtual users over three minutes, holds the plateau for fifteen, and winds down. The plateau is where your measurements mean something: caches are warm, connection pools are at working depth, and the system is in steady state. For a stress test, replace the single plateau with an ascending staircase, 100, then 200, then 400, each held for a few minutes, and watch where response times bend, per the profiles in our load vs stress guide.

Realistic tests also follow a journey rather than one URL. Inside the default function, chain the steps a user takes, home page, category, product, add to cart, with sleeps between them, and vary the data: pick product IDs from an array with a random index, or load a shared JSON file with SharedArray so a thousand virtual users draw from one pool of test data without duplicating memory.

Step 4: Validate Responses with Checks

A server melting under load often returns error pages quickly, and a test that only measures speed will cheerfully report improved response times while everything burns. Checks prevent this:

import { check } from 'k6';

const res = http.get('https://staging.example.com/product/42');
check(res, {
  'status is 200': (r) => r.status === 200,
  'has product title': (r) => r.body.includes('product-title'),
});

Checks record pass rates without aborting the iteration, mirroring reality where one failed request does not stop a user's session. The end-of-run summary shows each check's success percentage, and a plateau where "status is 200" drops to 96% is the story of the test, whatever the latency numbers say. Checks feed the http_req_failed metric that thresholds gate on next.

Step 5: Thresholds Turn Tests into Gates

Thresholds are k6's pass/fail mechanism and its best feature for automation:

export const options = {
  stages: [ /* as above */ ],
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1200'],
    http_req_failed: ['rate<0.01'],
  },
};

This declares the test failed if p95 latency reaches 500 milliseconds, p99 reaches 1.2 seconds, or more than 1% of requests fail. When a threshold breaks, k6 exits with a nonzero code, which any CI system reads as a failed job with no extra tooling. You can scope thresholds to specific journey steps by tagging requests and thresholding on the tag, so the checkout step gets a stricter budget than the marketing pages.

Thresholds are also what make miniature k6 tests work as merge gates: two minutes at 20 virtual users with tight thresholds catches a 3x latency regression the day it is written, the pattern covered in our CI/CD test automation guide.

Step 6: Read Results and Go Deeper

The terminal summary at the end of a run reports, per metric, the average, median, p90, p95, and max, plus throughput and check pass rates. Read p95 and p99 against your targets and treat averages as trivia. http_req_duration is the headline metric, but its components, http_req_connecting, http_req_tls_handshaking, http_req_waiting (time to first byte), separate network cost from server cost when you need to know where the milliseconds went.

For anything longer than a smoke test, stream results somewhere visual: k6 run --out influxdb=... or the Prometheus remote write output feeds live dashboards in Grafana, where load metrics sit on the same timeline as your server's CPU, memory, and database panels, and correlation becomes a matter of looking. The --out json option writes every data point for custom analysis.

Two directions to grow from here. Grafana Cloud k6 runs your same scripts from managed load generators in multiple regions with hosted dashboards, the upgrade path when one machine or one location stops being enough. And the k6 browser module drives real headless Chrome sessions inside tests, capturing frontend metrics like LCP for a handful of browser-level users while the HTTP engine supplies the bulk load, bridging toward the frontend techniques in our Playwright performance guide.

A Complete Example

Everything above, assembled into one realistic script:

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

const products = new SharedArray('products', () => JSON.parse(open('./products.json')));

export const options = {
  stages: [
    { duration: '3m', target: 100 },
    { duration: '15m', target: 100 },
    { duration: '2m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  const home = http.get('https://staging.example.com/');
  check(home, { 'home 200': (r) => r.status === 200 });
  sleep(Math.random() * 3 + 2);

  const p = products[Math.floor(Math.random() * products.length)];
  const prod = http.get(`https://staging.example.com/product/${p.id}`);
  check(prod, { 'product 200': (r) => r.status === 200 });
  sleep(Math.random() * 3 + 2);
}

Note for anyone copying this into a file: the HTML entities above render as normal angle brackets and ampersands in the browser, write the actual characters in your script. This test ramps realistically, spreads traffic across products, validates every response, and fails loudly when latency or errors cross budget, which is the complete anatomy of a useful load test in about thirty lines.

Key Takeaway

k6 tests are JavaScript functions describing one user's journey, shaped into realistic traffic by ramping stages, validated by checks, and judged by thresholds on p95 latency and error rate. The nonzero exit on threshold failure makes k6 tests natural CI gates, and Grafana outputs put load results on the same dashboards as server metrics. Start with a staged journey test against staging, keep think time and data variety honest, and let thresholds, not averages, declare success.