LoadWave
Guides

YAML or Go?

What the declarative format can express, where it runs out of road, and how to keep load shape in YAML while behaviour lives in Go.

LoadWave takes tests in two forms. They are not competing options — most mature setups use both, for different parts of the same test.

The short answer

Use YAML when

The test is a sequence of requests where each one only needs values from the ones before it. Most smoke tests, health checks and read-heavy browse flows are exactly this.

Use Go when

The test needs a session, a branch, a retry, a real authentication flow, weighted decisions, or a custom metric. Anything with an `if` in it.

What YAML can do

A YAML scenario is a list of steps. Each step is a request or a pause, values carry forward through capture, and ${...} templating puts them back into later URLs, headers, query parameters and bodies.

scenarios:
  - name: browse
    steps:
      - name: list products
        get: /api/products
        expect: [200]
        capture:
          productId: items.0.id

      - think: 1s-3s

      - name: view product
        get: /api/products/${productId}
        expect: [200]

That covers a surprising amount. Combined with built-in variables like ${__vu} — unique across the whole fleet — you can give every simulated user its own account without writing a line of Go:

- name: login
  post: /api/auth/login
  json: { username: "loadtest-user-${__vu}", password: "correct-horse-battery-staple" }
  capture:
    token: data.token

The full surface is in the configuration reference.

Where it runs out of road

YAML has no conditionals, no loops, no error handling and no arithmetic. That is a deliberate ceiling, not a gap waiting to be filled — a configuration format that grows an expression language has become a programming language with worse tooling.

You have hit the ceiling when you want to:

  • Branch. Do one thing if the cart is empty, another if it is not.
  • Hold session state across iterations. Sign in once per user, then browse for the rest of the run. capture lives for one iteration; Go state lives for the virtual user's lifetime.
  • Authenticate with anything but a bearer token. Request signing, token refresh, a challenge-response.
  • Decide by weight inside a scenario. One in twenty visitors abandons at payment.
  • Emit a custom metric. The value of the basket, the size of a queue.
  • Parse a response properly. capture reads a path out of JSON; it does not decode into a struct you can reason about.

The usual split

Behaviour in Go, where it can be tested and reviewed like any other code. Load shape and pass/fail criteria in YAML, where they can be varied per environment without a rebuild.

A scenario entry with only a name refers to one compiled into the binary:

checkout.yaml
name: storefront-checkout
baseURL: https://staging.example.com

load:
  executor: ramping-vus
  stages:
    - { duration: 2m, target: 200 }
    - { duration: 10m, target: 200 }
    - { duration: 2m, target: 0 }

thresholds:
  - { metric: http_req_duration, stat: p95, op: "<", value: 800 }
  - { metric: cart_value, stat: p50, op: ">", value: 0 }

scenarios:
  # Weights set here override the ones declared in Go, so the traffic mix can
  # be retuned per environment without rebuilding.
  - name: browse
    weight: 4
  - name: checkout
    weight: 1
go build -o checkout ./cmd/checkout
./checkout run checkout.yaml --ui

This is the arrangement worth aiming for. The thing that changes often — how much load, for how long, and what counts as passing — stays in a file anyone can edit in a pull request. The thing that changes rarely and needs review stays in Go.

What running Go tests costs you

One thing, and it is worth knowing up front:

Every host in the fleet must run the same binary. The scenarios are compiled in, so the binary is the test. Ship one build to every load host rather than installing loadwave on each.

In exchange, it is impossible for one host to be quietly running last week's scenario. The agent advertises its version and the dashboard shows it, so a mismatch is visible immediately.

For a YAML-only test, any loadwave binary of the same version will do.

Next

On this page