Load Testing Architecture

Design and execute load tests that accurately predict production behavior. Covers load testing tools, test scenario design, realistic traffic patterns, performance baselines, bottleneck identification, and the patterns that prevent performance surprises at scale.

Load testing architecture defines how a system simulates real-world user traffic to evaluate performance under sustained or peak load. It matters when a service must handle thousands of concurrent users, when response time SLAs are critical, or when new features introduce latency bottlenecks. This architecture orchestrates the generation, execution, and analysis of synthetic workloads across distributed components—databases, services, caches, and external APIs—with measurable, repeatable, and observable results.

Defining the Test Scope

A load test begins with a clear definition of *what* is being measured and *who* is being simulated. The scope must answer: Which user journeys are included? What are the request patterns? How are user behaviors modeled?

User journeys are described as sequences of HTTP requests with timing and data dependencies. For example, a user logging in, browsing a product catalog, and adding an item to cart generates a series of requests. These are captured in a script format using standard HTTP methods, headers, and payloads.

{
  "name": "user-login-and-add-to-cart",
  "steps": [
    {
      "method": "POST",
      "path": "/api/v1/auth/login",
      "headers": {
        "Content-Type": "application/json"
      },
      "body": {
        "email": "{{user.email}}",
        "password": "{{user.password}}"
      },
      "variables": {
        "token": "response.headers.authorization"
      }
    },
    {
      "method": "GET",
      "path": "/api/v1/products?category=electronics",
      "headers": {
        "Authorization": "Bearer {{token}}"
      },
      "wait": 1500
    },
    {
      "method": "POST",
      "path": "/api/v1/cart/add",
      "headers": {
        "Authorization": "Bearer {{token}}",
        "Content-Type": "application/json"
      },
      "body": {
        "productId": 123,
        "quantity": 2
      }
    }
  ]
}

The script is executed by a load testing engine that can simulate thousands of virtual users (VUs) with realistic pacing and think times. Each VU is a stateful client that follows the script, possibly varying its behavior across repeated iterations.

Orchestrating Virtual Users

The core of load testing architecture is the management of virtual users. A VU is not a thread, but a full runtime context with state, timers, and a request history. The number of VUs, their ramp-up and ramp-down profiles, and their behavior during test execution are critical.

VUs are driven by a load profile defined in a configuration file. This file specifies how many users to start, how quickly to ramp them up, and how long to run.


engine: "k6"
duration: "10m"
ramp-up: "2m"
ramp-down: "3m"
vus: {
  "baseline": 100,
  "peak": 1000,
  "steady": 5000
}

# Define the test script
script: "scripts/user-login-and-add-to-cart.js"

# Add global variables
variables:
  user-count: 10000
  base-url: "https://api.example.com"

# Configure HTTP settings
http:
  timeout: "30s"
  retries: 3
  max-connections: 1000

The load profile is applied by a test orchestrator—either a CLI tool or a service—that manages the lifecycle of the test.

k6 run --config load-test-config.yaml \
  --env staging \
  --output "json:results/staging-2024-06-15.json" \
  scripts/user-login-and-add-to-cart.js

Each VU runs independently, but shares a pool of resources such as database connections, HTTP clients, and shared state. When a test runs for 10 minutes, the orchestrator tracks per-VU metrics: start time, duration, success rate, error counts, and latency per request.

Managing Data and State

Load tests are only as realistic as their data. A test that uses the same user credentials for all VUs produces skewed results. To simulate real user diversity, data is generated or pulled from external sources.

Test data is managed through a data provider that feeds user profiles, product catalogs, or session states to the test engine. The engine uses templates to populate request bodies, headers, and variables.

For example, a test using data from a CSV file:

email,password,city,age
[email protected],secret123,Seattle,32
[email protected],pass456,Denver,41
[email protected],myword,Chicago,28

The test engine reads the CSV and assigns one row per VU or cycles through it in a round-robin fashion.

{
  "data": {
    "source": "csv",
    "path": "data/users.csv",
    "mode": "round-robin"
  },
  "variables": {
    "user": "data.users"
  }
}

State is maintained across test steps using session cookies, JWT tokens, and database snapshots. A login step may store a JWT in a variable, which is then used in subsequent requests. The test engine ensures that each VU maintains its session state across multiple iterations.

When a test fails due to inconsistent state—e.g., a user logs in but cannot access the cart—the error log shows the full sequence of requests and the values of variables at each step.

Observing and Collecting Metrics

The test engine collects metrics at multiple levels: per-request, per-VU, per-test, and system-wide.

Per-request metrics include response time (latency), status code, body size, and error details. The engine logs these in structured format, typically JSON.

{
  "timestamp": "2024-06-15T10:30:15.423Z",
  "vu": 47,
  "iteration": 23,
  "request": {
    "name": "GET /api/v1/products",
    "method": "GET",
    "url": "https://api.example.com/api/v1/products",
    "status": 200,
    "duration": 1243,
    "responseSize": 2048,
    "error": null
  }
}

The test engine emits metrics in a standard format, often via a metrics endpoint or a streaming protocol.

# Start the test and stream metrics
k6 run -o prometheus:metrics/prometheus.json \
  --metrics-addr "0.0.0.0:9555" \
  scripts/user-login-and-add-to-cart.js

Metrics are exposed over HTTP and can be scraped by a monitoring system like Prometheus.

GET /metrics
HTTP/1.1 200 OK
Content-Type: text/plain; version=0.0.4

# HELP k6_http_req_duration_ms HTTP request duration in milliseconds
# TYPE k6_http_req_duration_ms histogram
k6_http_req_duration_ms_bucket{method="POST",path="/api/v1/auth/login",le="100"} 472
k6_http_req_duration_ms_bucket{method="POST",path="/api/v1/auth/login",le="200"} 831
k6_http_req_duration_ms_bucket{method="POST",path="/api/v1/auth/login",le="500"} 943
k6_http_req_duration_ms_bucket{method="POST",path="/api/v1/auth/login",le="+Inf"} 1000
k6_http_req_duration_ms_sum{method="POST",path="/api/v1/auth/login"} 142345
k6_http_req_duration_ms_count{method="POST",path="/api/v1/auth/login"} 1000

The test engine also logs errors that occur during execution.

{
  "timestamp": "2024-06-15T10:30:17.812Z",
  "vu": 103,
  "iteration": 54,
  "error": {
    "type": "HTTPError",
    "message": "Request failed with status 503",
    "request": {
      "method": "POST",
      "path": "/api/v1/cart/add",
      "body": {
        "productId": 123,
        "quantity": 2
      }
    },
    "response": {
      "status": 503,
      "body": "Service Unavailable",
      "headers": {
        "Content-Type": "text/plain"
      }
    }
  }
}

These logs are collected and stored in a durable location—typically a file system, S3 bucket, or a database—for later analysis.

Scaling the Load Engine

As test size grows, the load engine must scale across multiple machines or pods. The architecture supports distributed execution where the orchestrator coordinates a cluster of worker nodes.

The orchestrator runs a master process that schedules tasks, manages state, and collects results. Workers run the actual test scripts, processing requests and emitting metrics.

Each worker connects to the master over a lightweight protocol, such as gRPC or HTTP. The master sends the script, configuration, and data to the workers, which execute the test in parallel.

Workers may run on bare metal, VMs, or containerized environments.

# worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k6-workers
spec:
  replicas: 10
  selector:
    matchLabels:
      app: k6-worker
  template:
    metadata:
      labels:
        app: k6-worker
    spec:
      containers:
        - name: k6
          image: grafana/k6:latest
          command:
            - "/bin/k6"
            - "run"
            - "--config"
            - "/etc/k6/config.yaml"
            - "/scripts/user-login-and-add-to-cart.js"
          env:
            - name: K6_MASTER_ADDR
              value: "k6-master.example.com:50051"
            - name: K6_ENV
              value: "staging"
          volumeMounts:
            - name: scripts
              mountPath: "/scripts"
            - name: config
              mountPath: "/etc/k6"
      volumes:
        - name: scripts
          configMap:
            name: k6-scripts
        - name: config
          configMap:
            name: k6-config

The master aggregates metrics from all workers, merges log files, and produces a unified report.

When the test runs for 10 minutes, the master tracks the number of requests per second, average latency, error rate, and throughput across the entire cluster.

Handling Failure and Drift

A load test silently fails when the system under test degrades but the test does not detect it. Common failure modes include:

The test engine validates responses using assertions. These are expressions evaluated after each request.

// In k6 script
import { check } from 'k6';

export default function () {
  const res = http.get('/api/v1/products');

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response has at least 10 products': (r) => JSON.parse(r.body).length >= 10,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'response contains total count': (r) => r.json().total !== undefined
  });
}

When an assertion fails, the engine records the failure with full context: request, response, variables, and timestamps.

Failures are collected and reported in a summary.

{
  "summary": {
    "total": 10000,
    "passed": 9234,
    "failed": 766,
    "failures": [
      {
        "name": "response time < 500ms",
        "count": 421,
        "details": [
          {
            "vu": 78,
            "iteration": 12,
            "duration": 842,
            "expected": 500,
            "actual": 842
          }
        ]
      },
      {
        "name": "response contains total count",
        "count": 345,
        "details": [
          {
            "vu": 34,
            "iteration": 45,
            "response": {
              "products": [...],
              "total": 15
            },
            "path": "total"
          }
        ]
      }
    ]
  }
}

The test architecture must handle drift—small but cumulative differences over time. For example, a test that runs for 10 minutes may have 10% of VUs experiencing session expiration, leading to a spike in login requests. The architecture accounts for this by enabling session tracking, automatic retries, and health checks.

Integrating with CI/CD

Load tests are most effective when integrated into a CI/CD pipeline. A test run is triggered on every pull request, merge to main, or release.

A CI job runs the load test and uploads results to a storage system, then updates a dashboard or sends alerts.

# .github/workflows/load-test.yml
name: Load Test
on: [push, pull_request]

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run load test
        run: |
          k6 run \
            --config load-test-config.yaml \
            --env production \
            --output "json:results/$(date +%Y-%m-%d-%H-%M).json" \
            scripts/user-login-and-add-to-cart.js
      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: load-test-results
          path: results/
      - name: Send results to dashboard
        run: |
          curl -X POST -H "Content-Type: application/json" \
            -d @results/2024-06-15-10-30.json \
            https://dashboard.example.com/api/test-results

The architecture ensures that load tests are not one-off tasks, but recurring, repeatable, and versioned components of the system’s quality assurance.

Summary

Load testing architecture is the framework for simulating real user behavior at scale. It defines how tests are structured, how virtual users are managed, how data and state are handled, how metrics are collected, and how failures are detected and reported. It enables teams to validate system performance with precision, ensuring that a service not only works, but works well under real load.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.