QA Engineer Skills 2026QA-2026k6 and Locust: Modern Load Testing Tools

k6 and Locust: Modern Load Testing Tools

k6 (Grafana k6)

k6 is a developer-centric load testing tool written in Go with JavaScript scripting. It has become the industry standard for teams practicing shift-left performance testing. Its key strengths are low resource footprint, native CI integration, and a scripting model that developers actually enjoy using.

Why k6 Wins for CI/CD

  • Single binary. No JVM, no dependencies. Download and run.
  • JavaScript scripting. Familiar syntax for most development teams.
  • Threshold-based pass/fail. Define SLOs directly in the test -- if they fail, the exit code is non-zero, and CI fails.
  • Built-in metrics export. Stream to Grafana, Datadog, Prometheus, or JSON.
  • Scenarios engine. Model multiple traffic patterns in a single test file.

Complete k6 Example: E-Commerce Load Test

// k6-load-test.js -- Realistic e-commerce load test with multiple scenarios
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend } from 'k6/metrics';

// Custom metrics for business-specific SLOs
const errorRate = new Rate('errors');
const checkoutDuration = new Trend('checkout_duration');

// Traffic shaping: ramp up, sustain, spike, cool down
export const options = {
  scenarios: {
    // Steady-state browsing traffic (70% of users)
    browse: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 100 },   // ramp up
        { duration: '5m', target: 100 },   // steady state
        { duration: '1m', target: 300 },   // spike
        { duration: '5m', target: 100 },   // back to steady
        { duration: '2m', target: 0 },     // ramp down
      ],
      exec: 'browseProducts',
    },
    // API consumers hitting the search endpoint (30% of traffic)
    api_search: {
      executor: 'constant-arrival-rate',
      rate: 50,            // 50 requests per second
      timeUnit: '1s',
      duration: '15m',
      preAllocatedVUs: 20,
      maxVUs: 100,
      exec: 'searchAPI',
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1500'],  // SLO enforcement
    errors: ['rate<0.01'],                             // <1% error rate
    checkout_duration: ['p(95)<3000'],                 // checkout under 3s
  },
};

export function browseProducts() {
  group('Homepage', () => {
    const res = http.get('https://store.example.com/');
    check(res, {
      'homepage status 200': (r) => r.status === 200,
      'homepage loads under 2s': (r) => r.timings.duration < 2000,
    });
    errorRate.add(res.status >= 400);
  });

  sleep(Math.random() * 3 + 1); // realistic think time: 1-4 seconds

  group('Product Page', () => {
    const productId = Math.floor(Math.random() * 1000) + 1;
    const res = http.get(`https://store.example.com/products/${productId}`);
    check(res, {
      'product page status 200': (r) => r.status === 200,
      'has product title': (r) => r.body.includes('<h1'),
    });
    errorRate.add(res.status >= 400);
  });

  sleep(Math.random() * 5 + 2); // browsing think time: 2-7 seconds
}

export function searchAPI() {
  const queries = ['laptop', 'phone', 'headphones', 'keyboard', 'monitor'];
  const query = queries[Math.floor(Math.random() * queries.length)];

  const res = http.get(`https://api.store.example.com/search?q=${query}`, {
    headers: { 'Authorization': `Bearer ${__ENV.API_TOKEN}` },
  });

  check(res, {
    'search returns 200': (r) => r.status === 200,
    'search returns results': (r) => JSON.parse(r.body).results.length > 0,
    'search under 300ms': (r) => r.timings.duration < 300,
  });
  errorRate.add(res.status >= 400);
}

Running k6

# Local execution with default output
k6 run k6-load-test.js

# With environment variables for secrets
k6 run -e API_TOKEN=secret123 k6-load-test.js

# Output results to Grafana Cloud k6
k6 cloud k6-load-test.js

# Output to JSON for CI analysis
k6 run --out json=results.json k6-load-test.js

# Run with a specific scenario only
k6 run --scenario browse k6-load-test.js

k6 Executor Types

Understanding executors is essential for modeling realistic traffic:

Executor Controls Best For
shared-iterations Total iterations across all VUs Quick smoke tests
per-vu-iterations Iterations per VU Ensuring each VU runs N times
constant-vus Fixed VU count Steady-state testing
ramping-vus VU count over time Ramp-up/down patterns
constant-arrival-rate Fixed request rate SLO validation (RPS-based)
ramping-arrival-rate Request rate over time Finding the breaking point
externally-controlled Via REST API Dynamic load adjustment

Locust (Python-Based)

Locust uses Python classes to define user behavior. It is the preferred choice for teams with strong Python skills, complex scenario logic, or when you need to share code with your existing Python test infrastructure.

Why Choose Locust

  • Pure Python. Leverage existing libraries (requests, database clients, custom SDKs).
  • Distributed by default. Built-in master/worker architecture for horizontal scaling.
  • Real-time web UI. Monitor test progress and adjust parameters live.
  • Event hooks. Deep customization of request lifecycle, error handling, and reporting.

Complete Locust Example

# locustfile.py -- Multi-persona e-commerce load test
from locust import HttpUser, task, between, tag, events
import random
import logging

logger = logging.getLogger(__name__)


class BrowsingUser(HttpUser):
    """Simulates casual browsing behavior -- 70% of traffic."""
    wait_time = between(1, 5)
    weight = 7  # 70% of simulated users

    @task(5)
    def view_homepage(self):
        with self.client.get("/", name="Homepage", catch_response=True) as response:
            if response.status_code != 200:
                response.failure(f"Homepage returned {response.status_code}")
            elif "Welcome" not in response.text:
                response.failure("Homepage missing welcome text")

    @task(3)
    def view_product(self):
        product_id = random.randint(1, 1000)
        self.client.get(f"/products/{product_id}", name="/products/[id]")

    @task(1)
    @tag("checkout")
    def add_to_cart(self):
        self.client.post("/cart", json={"product_id": 42, "quantity": 1})

    def on_start(self):
        """Runs once per simulated user at start."""
        logger.info("BrowsingUser session started")


class APIUser(HttpUser):
    """Simulates API consumer traffic -- 30% of traffic."""
    wait_time = between(0.1, 0.5)
    weight = 3  # 30% of simulated users

    @task
    def search(self):
        query = random.choice(["laptop", "phone", "headphones", "keyboard"])
        self.client.get(f"/api/search?q={query}", name="API Search")

    @task
    def get_inventory(self):
        product_id = random.randint(1, 100)
        self.client.get(f"/api/inventory/{product_id}", name="API Inventory")


# Custom event listener for reporting
@events.request.add_listener
def on_request(request_type, name, response_time, response_length, exception, **kwargs):
    if response_time > 5000:
        logger.warning(f"Slow request: {name} took {response_time}ms")

Running Locust

# Local single-process run
locust -f locustfile.py --host=https://staging.example.com

# Headless mode for CI (no web UI)
locust -f locustfile.py --host=https://staging.example.com \
  --headless -u 100 -r 10 --run-time 5m

# Distributed mode: start master
locust -f locustfile.py --master --host=https://staging.example.com

# Distributed mode: start workers (run on multiple machines)
locust -f locustfile.py --worker --master-host=192.168.1.100

# Filter by tag
locust -f locustfile.py --tags checkout --host=https://staging.example.com

k6 vs Locust: Decision Framework

When choosing between k6 and Locust, consider your team's context:

Factor Choose k6 Choose Locust
Team language JavaScript/TypeScript Python
Primary use CI/CD automated gates Exploratory + CI
Protocol needs HTTP, gRPC, WebSocket HTTP + custom protocols via Python
Resource budget Tight (k6 is extremely efficient) Moderate
Real-time monitoring Grafana integration Built-in web UI
Distributed execution k6-operator on K8s Native master/worker
Scripting complexity Simple to moderate Moderate to complex
Existing test infra Node.js ecosystem Python ecosystem

Hybrid Approach

Many mature teams use both tools for different purposes:

  • k6 in CI for automated performance gates (threshold-based pass/fail)
  • Locust for exploratory testing where the interactive web UI and Python flexibility shine
  • k6 Cloud for large-scale tests when you need thousands of VUs across regions

The key is not which tool you pick -- it is that you pick one and integrate it into CI so that performance regressions are caught before production.