Frontend Labs
⋯

7. Open-Source Load Testing Tools — Walkthroughs

Sections 1–6 gave you the vocabulary and the scenario patterns; from here on it's hands-on. This section is a tool-by-tool walkthrough — pick the one that matches your target (raw HTTP throughput, distributed scripted scenarios, or raw TCP/UDP) rather than reading all of it linearly.

7.1 wrk (C) — high-performance HTTP benchmarking

Written in C on top of an epoll/kqueue event loop with a small Lua scripting layer. Best for raw HTTP/1.1 throughput/latency measurement with minimal generator overhead.

# install (macOS)
brew install wrk

# 30 seconds, 12 threads, 400 concurrent connections, print latency percentiles
wrk -t12 -c400 -d30s --latency http://localhost:8080/api/products

Custom Lua script to POST JSON with per-request randomized bodies (models varied request content, not identical repeated requests):

-- post.lua
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"

request = function()
  local id = math.random(1, 100000)
  local body = string.format('{"user_id": %d, "item": "sku-%d"}', id, id % 500)
  return wrk.format(nil, nil, nil, body)
end
wrk -t8 -c200 -d60s -s post.lua --latency http://localhost:8080/api/orders

Note: plain wrk is a closed-loop tool (§3.4) — for open-loop, constant-rate testing, use its fork wrk2, which adds -R <rate> and corrects for "coordinated omission" (a bias where slow responses cause the tool to under-sample exactly the slow period, making reported percentiles look artificially good).

7.2 hey (Go) — simple, single-binary HTTP load generator

go install github.com/rakyll/hey@latest

hey -z 30s -c 200 -m POST -T "application/json" \
    -d '{"user_id":1,"item":"sku-1"}' \
    http://localhost:8080/api/orders

Output includes a latency histogram and status code distribution — good for quick checks without writing a script.

7.3 vegeta (Go) — attack/report pipeline, built for open-loop rate testing

vegeta is designed around Little's Law-aware, constant-arrival-rate testing and a separate report step, which maps directly onto §10 (report generation).

go install github.com/tsenart/vegeta@latest

# targets file: METHOD URL pairs
cat <<EOF > targets.txt
GET http://localhost:8080/api/products/1
GET http://localhost:8080/api/products/2
POST http://localhost:8080/api/orders
Content-Type: application/json
@body.json
EOF

# open-loop attack at fixed 500 req/s for 60s, save results
vegeta attack -targets=targets.txt -rate=500 -duration=60s > results.bin

# text report: min/mean/p50/p95/p99/max, success ratio
vegeta report results.bin

# histogram of latency buckets
vegeta report -type='hist[0,10ms,50ms,100ms,200ms,500ms,1s]' results.bin

# plot: interactive HTML latency-over-time chart
vegeta plot results.bin > plot.html

7.4 k6 (Go core, JavaScript scripting) — modern scriptable load testing

The most widely adopted modern tool; scenarios, thresholds (pass/fail SLAs baked into the test), and multiple executor types (constant VUs, ramping VUs, constant-arrival-rate, ramping-arrival-rate) make it well-suited to every pattern in §4.3.

// flash_sale.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';

const errorRate = new Rate('errors');
const checkoutLatency = new Trend('checkout_latency');

export const options = {
  scenarios: {
    browse: {
      executor: 'ramping-arrival-rate',
      startRate: 50,
      timeUnit: '1s',
      preAllocatedVUs: 2000,
      maxVUs: 5000,
      stages: [
        { target: 50, duration: '10s' },   // baseline
        { target: 5000, duration: '15s' }, // flash-sale spike
        { target: 5000, duration: '30s' }, // sustained peak
        { target: 0, duration: '10s' },    // drain
      ],
      exec: 'browse',
    },
    checkout: {
      executor: 'ramping-arrival-rate',
      startRate: 2,
      timeUnit: '1s',
      preAllocatedVUs: 200,
      maxVUs: 500,
      stages: [
        { target: 2, duration: '10s' },
        { target: 300, duration: '15s' },
        { target: 300, duration: '30s' },
        { target: 0, duration: '10s' },
      ],
      exec: 'checkout',
    },
  },
  thresholds: {
    'http_req_duration{scenario:browse}': ['p(95)<300'],
    'http_req_duration{scenario:checkout}': ['p(99)<1500'],
    errors: ['rate<0.01'],
  },
};

export function browse() {
  const res = http.get('http://localhost:8080/api/products/42');
  check(res, { 'status is 200': (r) => r.status === 200 });
  errorRate.add(res.status !== 200);
}

export function checkout() {
  const payload = JSON.stringify({ user_id: __VU, item: 'sku-limited-42' });
  const res = http.post('http://localhost:8080/api/orders', payload, {
    headers: { 'Content-Type': 'application/json' },
  });
  checkoutLatency.add(res.timings.duration);
  check(res, { 'checkout ok or sold-out': (r) => [200, 409].includes(r.status) });
  errorRate.add(![200, 409].includes(res.status));
}
k6 run --out json=results.json flash_sale.js

k6's thresholds block is the key feature for CI gating: the run exits non-zero if p95/p99 SLAs are violated, so this drops directly into a CI/CD pipeline as a performance regression gate.

7.5 Locust (Python) — code-first, stateful user-behavior simulation

Best when scenarios are inherently stateful/sequential (login → browse → hold seat → pay, §6.2) and you want to express that logic in a real programming language rather than a DSL.

# locustfile.py
from locust import HttpUser, task, between, events
import random

class ShopperUser(HttpUser):
    wait_time = between(1, 3)   # closed-loop "think time" between actions

    def on_start(self):
        resp = self.client.post("/api/login", json={"user": f"user{random.randint(1,100000)}"})
        self.token = resp.json().get("token")
        self.client.headers.update({"Authorization": f"Bearer {self.token}"})

    @task(9)
    def browse_product(self):
        product_id = random.randint(1, 5000)
        self.client.get(f"/api/products/{product_id}", name="/api/products/[id]")

    @task(1)
    def checkout(self):
        payload = {"item": "sku-limited-42", "qty": 1}
        with self.client.post("/api/orders", json=payload, catch_response=True) as resp:
            if resp.status_code == 409:
                resp.success()  # sold out is an expected outcome, not a failure
            elif resp.status_code != 200:
                resp.failure(f"unexpected status {resp.status_code}")

@events.quitting.add_listener
def _(environment, **kwargs):
    if environment.stats.total.fail_ratio > 0.01:
        environment.process_exit_code = 1  # CI gate
pip install locust

# headless, distributed-ready, generates HTML + CSV reports (see §10.3)
locust -f locustfile.py --headless \
  --users 5000 --spawn-rate 200 --run-time 5m \
  --host http://localhost:8080 \
  --html report.html --csv results

For scale beyond one machine's CPU (Python's GIL limits single-process concurrency), Locust supports a distributed mode:

# on the master
locust -f locustfile.py --master

# on N worker machines/containers
locust -f locustfile.py --worker --master-host=<master-ip>

7.6 Goose (Rust) — Locust-inspired, for CPU-efficient high-concurrency tests

When Python's per-process concurrency ceiling becomes the bottleneck (i.e., your load generator becomes the thing under test instead of the target service), Goose gives Locust-style ergonomics with Rust/Tokio throughput.

// Cargo.toml: goose = "0.17"
use goose::prelude::*;

async fn browse_product(user: &mut GooseUser) -> TransactionResult {
    let product_id = rand::random::<u32>() % 5000 + 1;
    let _goose = user.get(&format!("/api/products/{product_id}")).await?;
    Ok(())
}

async fn checkout(user: &mut GooseUser) -> TransactionResult {
    let body = serde_json::json!({ "item": "sku-limited-42", "qty": 1 });
    let _goose = user.post_json("/api/orders", &body).await?;
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), GooseError> {
    GooseAttack::initialize()?
        .register_scenario(
            scenario!("Shopper")
                .register_transaction(transaction!(browse_product).set_weight(9)?)
                .register_transaction(transaction!(checkout).set_weight(1)?),
        )
        .execute()
        .await?;
    Ok(())
}
cargo run --release -- --host http://localhost:8080 \
  --users 20000 --run-time 5m --report-file report.html

7.7 ghz (Go) — gRPC load testing

go install github.com/bojand/ghz/cmd/ghz@latest

ghz --insecure \
  --proto ./order.proto --call order.OrderService.CreateOrder \
  -d '{"user_id":1,"item":"sku-1"}' \
  -c 200 -n 100000 --connections 20 \
  --format html -o report.html \
  localhost:50051

--connections 20 with -c 200 models 200 concurrent callers spread over 20 HTTP/2 connections (10 streams/connection) — mirroring §5.4's guidance to avoid funneling all concurrency through one connection.

7.8 Apache JMeter (Java) — GUI + enterprise protocol coverage

Still widely used where teams need a GUI test-plan builder, non-HTTP protocol support (JDBC, JMS, LDAP, FTP), or existing organizational JMeter test-plan investment. CLI mode for CI:

jmeter -n -t flash_sale.jmx -l results.jtl -e -o report_dir/

-e -o auto-generates an HTML dashboard report from the results file (see §10).

7.9 iperf3 (C) — pure network layer, no application involved

Before blaming your application, prove what the network itself can do. iperf3 measures raw TCP/UDP throughput and jitter between two hosts with no HTTP/app overhead — the baseline every other number in this guide should be compared against.

# on the server
iperf3 -s

# on the client: 30s TCP throughput test
iperf3 -c server_ip -t 30

# UDP test with specified bandwidth, reports jitter + packet loss - useful for
# validating the "network" side of a video-streaming or VoIP load test (§6.5)
iperf3 -c server_ip -u -b 50M -t 30

If iperf3 shows you can only push 20 Mbps between two regions, no amount of API optimization will make a 100 Mbps application-level load test pass — fix network path/MTU/routing first.

7.10 Tool selection cheat-sheet

Need Tool
Quick single-endpoint smoke test hey, ab
Raw max-throughput HTTP/1.1 benchmark wrk
Open-loop constant-rate testing, avoid coordinated omission wrk2, vegeta, k6 arrival-rate executor
Complex, stateful, multi-step user journeys Locust (Python), Goose (Rust), k6 (JS)
CI-gated performance regression testing k6 (thresholds), Locust (process_exit_code)
gRPC services ghz
Distributed, massive-scale generation Locust distributed mode, k6 Cloud/Operator, Goose
Raw network baseline (no app layer) iperf3
GUI test-plan / enterprise protocols JMeter
Network condition simulation (latency/loss/jitter) tc netem, Toxiproxy, Comcast

Generated from real-life-scale-guide.md · 8 Oct 2026 16:36