LoadWave
Reference

Go SDK

The public API surface of pkg/loadwave — Scenario, VU, HTTPClient, Request, Response, Recorder and the rest.

github.com/SnowyFoxStudios/LoadWave/pkg/loadwave is the public API for writing load tests in Go. It depends on nothing internal, so a scenario under unit test pulls in no coordinator.

github.com/SnowyFoxStudios/LoadWave/pkg/loadwave/run is separate so that the SDK stays free of the CLI's dependencies.

This page is the shape of the API. The generated reference, with every method signature, is on pkg.go.dev. For how to use it, see Writing tests in Go.

Entry points

func Register(s Scenario)                       // add to the default registry
func Main()                                     // in package run — does not return
func Execute(registry *loadwave.Registry) int   // in package run — returns the exit code

run.Main() runs the command line against the default registry and exits. run.Execute returns the code instead, for a program with cleanup of its own:

func main() {
    code := run.Execute(loadwave.Default)
    closeThings()
    os.Exit(code)
}

Exit codes are part of the contract: 0 clean, 1 the command failed, 2 a threshold was breached.

Scenario

Prop

Type

The hooks fire in this order:

Setup           once per worker process, before any VU starts
  OnVUStart     once per virtual user
    Run         repeatedly, until the profile ends
  OnVUStop      once per virtual user
Teardown        once per worker process, after all VUs have stopped

Setup and Teardown run once per worker process, not once per run. A run spread over four processes calls Setup four times. Anything that must happen exactly once for the whole run belongs outside the scenario.

Scenario.Validate() reports whether it is well formed; EffectiveWeight() resolves the zero value to 1.

VU

One virtual user: a single simulated client, executing its scenario in a loop for the lifetime of the run.

Requests and pauses

MethodWhat it does
HTTP() *HTTPClientThe VU's client. Panics if the run was built without one, which only happens in hand-made test VUs.
Think(ctx, d)Pause. Interruptible, and excluded from iteration_duration.
ThinkBetween(ctx, min, max)Pause drawn uniformly from the range. Prefer this.

Assertions and failures

MethodWhat it does
Check(name, ok) boolRecords a named assertion and returns its result. Does not fail the iteration by itself.
Checkf(name, ok, format, args...) boolCheck with a message logged on failure. The message is not a label, so it may contain values.
Fail(err)Records an error against this iteration without abandoning it.

Identity and state

MethodWhat it does
ID() int64Run-wide unique id, across the whole fleet.
Index() intPosition within this worker process.
Iteration() intZero-based index of the current iteration.
Scenario() stringName of the scenario being executed.
Shard() ShardThe data partition assigned to this VU.
SetState(key, value) / State(key)Values that survive across iterations of this VU.
Rand() *rand.RandThis VU's own generator. Not shared, so no locking and no contention.
Log() *slog.LoggerA logger already tagged with the VU id and scenario.

StateOf[T](vu, key) (T, bool) is the generic accessor, returning the zero value of T when the key is absent or holds a different type.

Metrics

MethodWhat it does
Metrics() RecorderThe sink for custom metrics.
Labels() LabelsThe tags currently applied to this VU's observations.
Tag(key, value)Adds a label to everything this VU emits for the rest of the iteration.

Exactly one goroutine ever touches a given VU, so nothing on it is synchronised. Do not hand a VU to a goroutine you spawn yourself; if an iteration needs concurrency, share only immutable values.

BeginIteration, EndIteration and Close exist on the type but belong to the engine — scenarios do not call them.

HTTPClient

func (c *HTTPClient) Get(ctx, rawURL) (*Response, error)
func (c *HTTPClient) Delete(ctx, rawURL) (*Response, error)
func (c *HTTPClient) PostJSON(ctx, rawURL, body any) (*Response, error)
func (c *HTTPClient) PutJSON(ctx, rawURL, body any) (*Response, error)
func (c *HTTPClient) Do(ctx, req Request) (*Response, error)

The returned *Response is never nil. The returned error is non-nil only for transport-level failures: a 4xx or 5xx comes back with a nil error and the status set, because at load-test altitude a 500 is a measurement, not an exception.

HTTPClientFactory builds one client per virtual user, so the expensive shareable part — the transport and its connection pool — is built once per worker process while the cheap per-user part is built ten thousand times:

factory, err := loadwave.NewHTTPClientFactory(loadwave.HTTPOptions{BaseURL: url})
defer factory.Close()
client := factory.New()

Request

Prop

Type

Response

Always non-nil, including when the transport failed, so a scenario can branch on Err or OK() without a nil check.

FieldWhat it is
StatusCode, Status, Proto, HeaderThe usual. StatusCode is 0 when no response arrived.
Body []byte, Truncated boolThe body, and whether it exceeded MaxBodyBytes. Empty when DiscardBody is set.
DurationThe whole request, first byte written to last byte read.
TTFBThe wait for the first response byte.
ConnectingTCP setup time. Zero when the connection was reused.
TLSHandshakeHandshake time. Zero for plaintext or a reused connection.
ConnReusedWhether a pooled connection served this request.
BytesIn, BytesOutByte counts.
ErrThe transport error. Nil when a response was received — an HTTP 500 is not an error here.

Methods: OK() bool, JSON(v any) error, Text() string, String() string.

HTTPOptions

The Go form of the http configuration block, with the same defaults and the same reasoning. Zero values apply the defaults:

const (
    DefaultHTTPTimeout         = 30 * time.Second
    DefaultMaxIdleConnsPerHost = 512
    DefaultMaxRedirects        = 10
    DefaultMaxBodyBytes        = 4 << 20 // 4 MiB
)

const DefaultBetweenRequests = time.Second

Three fields have no YAML equivalent:

FieldWhat it does
Trace *boolCollects connection-level timings via httptrace. Costs a few hundred nanoseconds per request. On by default.
IsSuccess func(*Response) boolDecides whether a response counts toward the failure rate. DefaultIsSuccess is transport-succeeded and status below 400.
NoBetweenRequests boolDisables pacing entirely. A separate flag rather than a zero duration, because zero has to keep meaning "not configured".

Recorder

The sink a scenario writes observations to. The engine supplies the implementation; scenarios reach it through VU.Metrics().

type Recorder interface {
    Count(metric string, labels Labels, delta float64)
    Trend(metric string, labels Labels, value float64)
    Rate(metric string, labels Labels, ok bool)
    Gauge(metric string, labels Labels, value float64)
}

DiscardRecorder() drops every observation — the default in a hand-built test VU.

FailureReporter is a separate optional interface receiving Failure values; a recorder that does not implement it simply gets no failure samples.

Labels

An immutable, pre-hashed set of metric tags. Tags are attached to every single observation, so they are meant to be built once and reused for the millions of samples that follow.

func NewLabels(kv ...string) Labels          // panics on an odd count
func LabelsFromMap(m map[string]string) Labels
func (l Labels) With(kv ...string) Labels    // returns a copy; never mutates

Also: Get, Len, All, Map, Hash, Equal, String. Because the value is immutable it may be shared across virtual users without synchronisation.

The constants LabelScenario, LabelName, LabelMethod, LabelStatus, LabelError, LabelCheck and LabelExpected name the standard keys.

Pause

func NewPause(d time.Duration) Pause
func NewPauseRange(min, max time.Duration) Pause
func ParsePause(spec string) (Pause, error)   // "500ms", "1s", "1s-3s"

The zero Pause means no delay. ParsePause("") is an error rather than a zero pause: at every call site the difference between "not specified" and "explicitly none" matters, and only the caller knows which an empty field means.

Shard and Slice

type Shard struct {
    Index uint32
    Count uint32
}

func (s Shard) Owns(i int) bool
func Slice[T any](s Shard, items []T) []T

The coordinator hands each node a static (Index, Count) pair at start, so nodes partition fixtures arithmetically with no runtime coordination. Slice returns this shard's elements, preserving order and aliasing nothing.

Registry

Most programs use the package-level Default registry via loadwave.Register. An explicit registry is useful in tests, where global state between cases is a hazard.

func NewRegistry() *Registry
func (r *Registry) Register(s Scenario) error
func (r *Registry) MustRegister(s Scenario)
func (r *Registry) Lookup(name string) (Scenario, bool)
func (r *Registry) Names() []string   // sorted, so output is deterministic
func (r *Registry) Len() int
func (r *Registry) Clone() *Registry

Metric name constants

Every built-in metric has a constant — MetricHTTPReqs, MetricHTTPReqDuration, MetricHTTPReqWaiting, MetricHTTPReqConnecting, MetricHTTPReqTLS, MetricHTTPReqFailed, MetricHTTPReqBytesIn, MetricHTTPReqBytesOut, MetricIterations, MetricIterationDuration, MetricIterationFailed, MetricVUs, MetricChecks, MetricErrors.

Scenarios may emit their own metrics alongside these, but the dashboard and the default threshold set are written against these names, so a custom HTTP-like protocol is best served by reusing them.

Helpers

FunctionWhat it does
NewVU(cfg VUConfig) *VUBuilds a virtual user. Exported so scenarios can be tested without infrastructure.
DeriveRequestName(method, path)The low-cardinality label heuristic, exported so you can see what it will produce.
DefaultIsSuccess(*Response) boolTransport succeeded and status below 400.
TruncateMessage(text) stringCollapses text to a single line and clips it to MaxFailureMessage (240).

See also

On this page