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
- Read the metric names in the message and compare actual values against thresholds in the end-of-test summary
- Fix the performance problem or reduce the load (fewer VUs, lower arrival rate) so thresholds pass
- Relax the threshold expressions in options.thresholds to realistic targets
- 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
- Baseline the system and calibrate thresholds from measured p95/p99 before enforcing them
- Prefer rate thresholds with tolerance (e.g. 'rate<0.01') over absolute latency in noisy environments
- Use abortOnFail deliberately — it stops the run early instead of just failing the exit code
- Keep thresholds in version control and review them like code
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
- parsing metric name failed
- invalid threshold
- Unexpected end of selector while parsing selector `${selecto
- 99
- missing init environment
AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15).
Data as JSON: /api/errors/714425729d8824b3.
Report an issue: GitHub.