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
| Tool | Pick when |
|---|---|
| k6 | JS team, modern CI, code-as-config, simple grand reports |
| Gatling | JVM shop; best-looking HTML reports; high throughput per host |
| Locust | Python team; live web UI for ad-hoc |
| JMeter | Need 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.