grafana/k6 · error

99

99

Error message

thresholds on metrics '%s' have been crossed

What it means

After a test finishes and all samples are flushed to outputs, k6 performs a final threshold evaluation via handleFinalThresholdCalculation. If any threshold expression was crossed, k6 returns this error listing the breached metric names and exits with code 99 (ThresholdsHaveFailed), with abort reason AbortedByThresholdsAfterTestEnd. This is k6's intended mechanism for making thresholds load-bearing in CI.

Source

Thrown at internal/cmd/run.go:364

	}()

	if thresholdsEnabled {
		finalizeThresholds := metricsEngine.StartThresholdCalculations(
			metricsIngester, runAbort, executionState.GetCurrentTestRunDuration,
		)
		handleFinalThresholdCalculation := func() {
			// This gets called after the Samples channel has been closed and
			// the OutputManager has flushed all of the cached samples to
			// outputs (including MetricsEngine's ingester). So we are sure
			// there won't be any more metrics being sent.
			logger.Debug("Finalizing thresholds...")
			breachedThresholds := finalizeThresholds()
			if len(breachedThresholds) == 0 {
				return
			}
			tErr := errext.WithAbortReasonIfNone(
				errext.WithExitCodeIfNone(
					fmt.Errorf("thresholds on metrics '%s' have been crossed", strings.Join(breachedThresholds, ", ")),
					exitcodes.ThresholdsHaveFailed,
				), errext.AbortedByThresholdsAfterTestEnd)

			if err == nil {
				err = tErr
			} else {
				logger.WithError(tErr).Debug("Crossed thresholds, but test already exited with another error")
			}
		}
		if finalizeThresholds != nil {
			defer handleFinalThresholdCalculation()
		}
	}

	defer func() {
		logger.Debug("Waiting for metrics and traces processing to finish...")
		close(samples)

View on GitHub (pinned to 93accf6570)

Solutions

  1. Read the metric names in the message and compare actual values against thresholds in the end-of-test summary
  2. Fix the performance problem or reduce the load (fewer VUs, lower arrival rate) so thresholds pass
  3. Relax the threshold expressions in options.thresholds to realistic targets
  4. For exploratory runs only, skip threshold evaluation with --no-thresholds or K6_NO_THRESHOLDS=true (not recommended for CI)

Example fix

// before
export const options = { thresholds: { http_req_duration: ['p(95)<500'] } };
// after (calibrated to measured performance)
export const options = { thresholds: { http_req_duration: ['p(95)<800'] } };
Defensive patterns

Strategy: validation

Validate before calling

# Review the effective thresholds before running
k6 inspect script.js | jq '.thresholds'
# Branch on the exit code so 99 (SLO breach) is distinct from other failures
k6 run script.js; rc=$?
if [ $rc -eq 99 ]; then echo 'quality gate failed: thresholds crossed'; exit 99; fi
exit $rc

Try / catch

In CI, branch on exit code: 0 = pass, 99 = thresholds crossed (quality gate), 104 = misconfiguration, 105 = canceled. Don't collapse them into one 'failed' bucket, and attach the end-of-test summary as build evidence.

Prevention

When it happens

Trigger: Running 'k6 run script.js' where options.thresholds contain conditions that evaluate true at end of test, e.g. http_req_duration: ['p(95)<500'] when measured p95 was above 500ms. The check runs in a deferred handler after the Samples channel has closed and the MetricsEngine ingester has drained.

Common situations: The system under test is slower than thresholds assume; thresholds are too strict for the configured load level; aborted runs re-evaluate thresholds on partial data; CI treats exit 99 as a build failure (usually the desired behavior).

Related errors


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