Diagnosing k6 Threshold Failures: From Red Output to Root Cause
A k6 threshold failure is a signal, not a verdict. This diagnostic guide helps you decide whether the application is genuinely too slow or the test itself is miscalibrated — with ordered checks, a symptom table, and escalation criteria.
02 Feb 2026, 01:11 UTC

Your k6 run finishes, the summary prints, and one or more lines show a red cross next to a threshold. The exit code is non-zero, your CI pipeline fails, and now you need to know: is the application actually too slow, or is the test wrong? This guide walks through that decision systematically.
The useful takeaway up front: a threshold failure is a signal, not a verdict. Roughly half the threshold failures teams chase turn out to be test-side problems — unrealistic SLAs baked into the script, thresholds applied to the wrong metric, or load profiles that don't match production. The other half are genuine performance regressions. Telling them apart quickly is the skill.
Recognizing the condition
A threshold failure looks like this in the end-of-test summary:
✗ http_req_duration..............: avg=812ms min=120ms med=640ms max=4.2s p(90)=1.8s p(95)=2.4s
✗ 'p(95)<500' p(95)=2.4s
✗ http_req_failed................: 12.40% ✓ 1240 ✗ 8760
✗ 'rate<0.01' rate=12.40%k6 exits with code 99 when any threshold fails (assuming you haven't overridden exit behavior). In CI, that's usually the first symptom anyone sees.
Cause and diagnostic table
| Symptom | Likely cause | First check |
|---|---|---|
| p(95) latency threshold fails only at peak VUs | Application saturates under load, or threshold set for single-user latency | Compare p(95) at low VU count vs peak |
| http_req_failed rate spikes with latency | Timeouts or connection exhaustion upstream | Check web server / load balancer logs for 502/504 |
| Threshold fails even at 1 VU | Threshold value unrealistic, or environment problem (cold cache, wrong region) | Run a single iteration and inspect timings |
| Failure only on one endpoint or group | Endpoint-specific bottleneck (slow query, N+1) | Use per-group thresholds or tags to isolate |
| Thresholds pass locally, fail in CI | CI runner resource contention, network distance to target | Check where the load generator runs relative to the target |
| Threshold on a Trend submetric behaves unexpectedly after upgrade | Threshold evaluation changes across k6 versions | Pin and verify your k6 version; review release notes |
Ordered checks
1. Confirm what actually failed
Export the full summary so you can inspect every metric, not just the failing lines. Run from the machine where you execute k6 (no special permissions needed):
k6 run --summary-export=summary.json script.jsThen inspect summary.json — it contains the full metric tree including values for metrics that didn't have thresholds. Look at the whole latency distribution, not only p(95): if p(50) is fine but max is enormous, you have a tail-latency problem (queueing, GC pauses, lock contention), not uniform slowness.
2. Sanity-check the threshold itself
Open the script and read the threshold definition:
export const options = {
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};Ask: where did 500ms come from? If it's a guess rather than a documented SLA, the threshold may be wrong, not the app. Also confirm the aggregation: p(95) over the whole run can hide a slow ramp-up; consider 'p(95)<500' on a per-group Trend if only one flow matters. Note that threshold syntax and evaluation details have changed across k6 versions — verify against the docs for the version you run (k6 version).
3. Reproduce at minimal load
Run with 1 VU for a few iterations:
k6 run --vus 1 --iterations 20 script.jsIf the threshold still fails at 1 VU, the problem is environmental or the threshold is miscalibrated — not a load problem. If it passes, move to step 4.
4. Find the breaking point
Run a ramping profile and watch where metrics degrade:
stages: [
{ duration: '2m', target: 10 },
{ duration: '2m', target: 50 },
{ duration: '2m', target: 100 },
{ duration: '2m', target: 200 },
]The VU count where latency inflects upward is your current capacity ceiling. If that ceiling is below your target load, the application or infrastructure is the bottleneck — the test is doing its job.
5. Rule out the load generator
k6 itself can be the bottleneck. On the generator machine, watch CPU and memory during the run. A saturated generator reports inflated latencies. Signs: high CPU on the k6 process, rising vus but flat iterations rate, or dropped iterations. Fixes: reduce per-VU work (avoid heavy JS in the default function), use --discard-response-bodies if bodies aren't needed, or distribute the load across multiple generators.
Fixes tied to findings
- Threshold miscalibrated: set it from measured baseline plus headroom, or from the actual SLA. Document the source in a comment next to the threshold.
- Tail latency only: investigate the slowest requests — enable per-request logging on the app, correlate with
http_req_durationmax and p(99). - Failure rate with latency: almost always upstream timeouts. Check gateway/proxy timeout settings against observed p(99).
- Generator saturation: scale out the generator before concluding anything about the app.
- Real capacity shortfall: profile the application at the inflection VU count; the threshold failure is confirmed as genuine.
Escalation criteria
Escalate beyond the test owner when: thresholds fail at or below documented production load after the generator is ruled out; failures correlate with infrastructure saturation (CPU, DB connections, thread pools) visible in monitoring; or the breaking point regressed versus a previous baseline run. At that point it is an application or capacity problem, and the k6 artifacts (summary export, ramp profile, inflection point) are the evidence to hand over.
Limitations
Threshold behavior and syntax vary across k6 versions; verify against the documentation for your installed version before relying on specific submetric support. Numbers in examples are illustrative, not measured. Never loosen a threshold just to make CI green without recording why.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.