grafana/k6 · error

error for stage %d: %w

Error message

error for stage %d: %w

What it means

Emitted while parsing the repeatable --stage flag (e.g. `k6 run --stage 1m:10,...`). Each stage string is split on ':' and decoded by lib.Stage.UnmarshalText: the left side must be a Go duration (time.ParseDuration) and the right side a plain integer VU target. Any duration without a unit, non-numeric target, or extra malformed text fails with this per-index wrap, where %d is the position (0-based) in the --stage list.

Source

Thrown at internal/cmd/options.go:114

		NoConnectionReuse:       getNullBool(flags, "no-connection-reuse"),
		NoVUConnectionReuse:     getNullBool(flags, "no-vu-connection-reuse"),
		MinIterationDuration:    getNullDuration(flags, "min-iteration-duration"),
		Throw:                   getNullBool(flags, "throw"),
		DiscardResponseBodies:   getNullBool(flags, "discard-response-bodies"),
		MetricSamplesBufferSize: null.NewInt(1000, false),
	}

	// Using Changed() because GetStringSlice() doesn't differentiate between empty and no value
	if flags.Changed("stage") {
		stageStrings, err := flags.GetStringSlice("stage")
		if err != nil {
			return opts, err
		}
		opts.Stages = []lib.Stage{}
		for i, s := range stageStrings {
			var stage lib.Stage
			if err := stage.UnmarshalText([]byte(s)); err != nil {
				return opts, fmt.Errorf("error for stage %d: %w", i, err)
			}
			if !stage.Duration.Valid {
				return opts, fmt.Errorf("stage %d doesn't have a specified duration", i)
			}
			opts.Stages = append(opts.Stages, stage)
		}
	}

	if flags.Changed("execution-segment") {
		executionSegmentStr, err := flags.GetString("execution-segment")
		if err != nil {
			return opts, err
		}
		segment := new(lib.ExecutionSegment)
		err = segment.UnmarshalText([]byte(executionSegmentStr))
		if err != nil {
			return opts, err
		}

View on GitHub (pinned to 93accf6570)

Solutions

  1. Use Go duration syntax on the left of ':' — always with a unit: `--stage 30s:10 --stage 1m:5 --stage 10s:0`
  2. Keep the target side a bare integer (VUs), no quotes/decimals
  3. Count from 0: the index in the message points at the exact --stage occurrence to fix
  4. Prefer a JSON/YAML options block (stages: [...]) when the ramp gets complex, instead of long flag chains

Example fix

# before
k6 run --stage 20:10 --stage 1m:5 script.js
# error: error for stage 0: time: missing unit in duration "20"

# after
k6 run --stage 20s:10 --stage 1m:5 script.js
Defensive patterns

Strategy: validation

Validate before calling

# Validate --stage strings: '<duration-with-unit>:<integer>' before running
for s in "${stages[@]}"; do
  [[ "$s" =~ ^[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h):[0-9]+$ ]] \
    || { echo "bad stage '$s' (want e.g. 30s:10)"; exit 1; }
done
k6 run ${stages[@]/#/--stage } script.js

Type guard

function isValidStageFlag(s) {
  return /^[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h):[0-9]+$/.test(s.trim());
}

Prevention

When it happens

Trigger: `--stage 10:5` (duration '10' has no unit, needs '10s'), `--stage 1m:five`, `--stage 1m:10:extra`, or a stage copied from JSON config syntax (`{"duration":"1m","target":10}`) pasted verbatim into the flag.

Common situations: Assuming seconds-by-default like some other tools; converting ramping-vus JSON options into CLI flags by hand; shell loops concatenating stage strings with stray separators.

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/91f7d82ad116ed9b. Report an issue: GitHub.