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 coderun.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 stoppedSetup 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
| Method | What it does |
|---|---|
HTTP() *HTTPClient | The 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
| Method | What it does |
|---|---|
Check(name, ok) bool | Records a named assertion and returns its result. Does not fail the iteration by itself. |
Checkf(name, ok, format, args...) bool | Check 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
| Method | What it does |
|---|---|
ID() int64 | Run-wide unique id, across the whole fleet. |
Index() int | Position within this worker process. |
Iteration() int | Zero-based index of the current iteration. |
Scenario() string | Name of the scenario being executed. |
Shard() Shard | The data partition assigned to this VU. |
SetState(key, value) / State(key) | Values that survive across iterations of this VU. |
Rand() *rand.Rand | This VU's own generator. Not shared, so no locking and no contention. |
Log() *slog.Logger | A 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
| Method | What it does |
|---|---|
Metrics() Recorder | The sink for custom metrics. |
Labels() Labels | The 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.
| Field | What it is |
|---|---|
StatusCode, Status, Proto, Header | The usual. StatusCode is 0 when no response arrived. |
Body []byte, Truncated bool | The body, and whether it exceeded MaxBodyBytes. Empty when DiscardBody is set. |
Duration | The whole request, first byte written to last byte read. |
TTFB | The wait for the first response byte. |
Connecting | TCP setup time. Zero when the connection was reused. |
TLSHandshake | Handshake time. Zero for plaintext or a reused connection. |
ConnReused | Whether a pooled connection served this request. |
BytesIn, BytesOut | Byte counts. |
Err | The 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.SecondThree fields have no YAML equivalent:
| Field | What it does |
|---|---|
Trace *bool | Collects connection-level timings via httptrace. Costs a few hundred nanoseconds per request. On by default. |
IsSuccess func(*Response) bool | Decides whether a response counts toward the failure rate. DefaultIsSuccess is transport-succeeded and status below 400. |
NoBetweenRequests bool | Disables 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 mutatesAlso: 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) []TThe 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() *RegistryMetric 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
| Function | What it does |
|---|---|
NewVU(cfg VUConfig) *VU | Builds 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) bool | Transport succeeded and status below 400. |
TruncateMessage(text) string | Collapses text to a single line and clips it to MaxFailureMessage (240). |