k6 & Gatling

54k6 & Gatling (modern load testing)

What you will master here

  • k6: install, JS scripts, scenarios, thresholds
  • Stages, executors, custom metrics
  • Output: cloud, InfluxDB+Grafana, JSON
  • Gatling: Scala/Java DSL, scenarios, injection profiles
  • Locust briefly
  • Choosing between them

54.1 k6 — install + first script

brew install k6                      # macOS
docker pull grafana/k6                # or via docker
// load.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 50,            // virtual users
  duration: '30s',
  thresholds: {
    http_req_failed:   ['rate<0.01'],          // <1% failures
    http_req_duration: ['p(95)<500', 'p(99)<1000'],
  },
};

export default function () {
  const res = http.get('https://api.example.com/products');
  check(res, {
    'status 200': (r) => r.status === 200,
    'has items':  (r) => JSON.parse(r.body).length > 0,
  });
  sleep(1);
}
k6 run load.js
k6 run --vus 100 --duration 1m load.js     # CLI overrides

54.2 Stages — ramped profiles

export const options = {
  stages: [
    { duration: '30s', target: 20 },    // ramp to 20 users over 30s
    { duration: '2m',  target: 20 },    // hold 20 for 2 min
    { duration: '30s', target: 100 },   // ramp to 100 over 30s
    { duration: '1m',  target: 100 },   // hold 100
    { duration: '30s', target: 0 },     // ramp down
  ],
};

54.3 Scenarios + executors

export const options = {
  scenarios: {
    smoke: {
      executor: 'constant-vus', vus: 5, duration: '1m',
      tags: { kind: 'smoke' },
    },
    load: {
      executor: 'ramping-arrival-rate',   // rate-based, not user-based
      startRate: 50, timeUnit: '1s',
      preAllocatedVUs: 100, maxVUs: 500,
      stages: [
        { duration: '30s', target: 50 },
        { duration: '5m',  target: 200 },
        { duration: '30s', target: 0 },
      ],
    },
  },
};

54.4 Custom metrics

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

const orderDuration = new Trend('order_duration');
const ordersCreated = new Counter('orders_created');
const failures = new Rate('failures');

export default function () {
  const start = Date.now();
  const res = http.post('/api/orders', JSON.stringify({/* ... */}));
  orderDuration.add(Date.now() - start);
  if (res.status === 201) ordersCreated.add(1);
  failures.add(res.status >= 400);
}

54.5 Output options

k6 run --out json=results.json load.js
k6 run --out influxdb=http://localhost:8086/k6 load.js   # → Grafana
k6 run --out cloud load.js                               # k6 Cloud SaaS

54.6 Browser load testing

k6-browser (built on Playwright) — runs real browser sessions under load. Heavier per VU but measures real Web Vitals under stress.

import { browser } from 'k6/browser';
export const options = {
  scenarios: { ui: { executor: 'shared-iterations', vus: 5, iterations: 20,
                     options: { browser: { type: 'chromium' }}}},
};
export default async function () {
  const page = await browser.newPage();
  await page.goto('https://app.example.com');
  await page.locator('text=Buy').click();
}

54.7 Gatling — Scala/Java DSL

// Java DSL (Gatling 3.7+)
public class BasicSim extends Simulation {
  HttpProtocolBuilder httpProtocol = http
      .baseUrl("https://api.example.com")
      .acceptHeader("application/json");

  ScenarioBuilder scn = scenario("Products")
      .exec(http("get products").get("/products")
          .check(status().is(200)));

  {
    setUp(scn.injectOpen(
        nothingFor(5),                       // wait 5s
        atOnceUsers(10),                     // ramp 10
        rampUsersPerSec(1).to(20).during(60) // ramp rps from 1 to 20 over 60s
    )).protocols(httpProtocol)
      .assertions(global().responseTime().percentile(95).lt(500));
  }
}

54.8 Locust

from locust import HttpUser, task, between

class WebUser(HttpUser):
    wait_time = between(1, 5)
    @task
    def view_products(self):
        self.client.get("/products")
    @task(3)
    def buy(self):
        self.client.post("/orders", json={"sku":"A","qty":1})

# Run: locust -f locustfile.py --headless -u 100 -r 10 -t 1m --host=https://app

54.9 Picking a tool

ToolPick when
k6JS team, modern CI, code-as-config, simple grand reports
GatlingJVM shop; best-looking HTML reports; high throughput per host
LocustPython team; live web UI for ad-hoc
JMeterNeed protocols beyond HTTP, legacy/non-tech testers

Module 55 — k6/Gatling Q&A

What are thresholds in k6?
Pass/fail criteria on metrics — http_req_duration: ['p(95)<500'] fails the test if 95th percentile exceeds 500 ms. Wired directly into CI exit code.
Closed vs open model load?
Closed (VUs/users) — fixed number of "users" looping. Open (arrival rate) — requests arrive at a fixed rate regardless of server response time. Open is more realistic for traffic-spike modelling; closed is simpler.
What's a k6 scenario?
A named workload pattern — executor + parameters. You can run multiple scenarios in one test (smoke + ramp + stress) and tag/separate metrics per scenario.
How do you output k6 results to a dashboard?
--out influxdb=... pushes metrics into InfluxDB; Grafana visualises. Or --out cloud for k6 Cloud (paid SaaS).
What's k6-browser for?
Real-browser load testing using Chromium (Playwright underneath). Measures actual rendered performance under load — not just API throughput.
Why use Gatling over JMeter?
Higher throughput per host (Akka-based asynchronous), code-first DSL (easier code review/refactor), gorgeous HTML reports. JMeter wins on protocol coverage and existing test plans.
What's Locust's live UI for?
Interactive web UI where you can ramp users up/down, watch live metrics, restart scenarios. Useful for exploratory load testing. Also has headless mode for CI.
How would you spike-test an API?
Configure a stage that goes from low → very high in seconds (e.g. 10 → 500 over 5s), holds briefly, then back. Assert thresholds for error rate and recovery latency.