QA How-To
K6 thresholds and checks (2026)
Learn k6 thresholds and checks for SLO gates: check API, tagged metrics, abortOnFail, custom Trends, CI exit codes, and interview Q&A for performance QA.
18 min read | 2,326 words
TL;DR
k6 thresholds and checks combine per-response assertions with end-of-test metric gates. Use check() for correctness under load, thresholds on latency and error rates for SLOs, and non-zero exit codes to fail CI when the contract breaks.
Key Takeaways
- Checks assert per-response correctness; thresholds gate aggregated metrics.
- Failed checks do not fail CI unless you threshold the checks rate or related metrics.
- Use p95/p99 on Trends for latency SLOs, not single-request duration checks alone.
- Tag requests so thresholds can isolate flows and endpoints.
- Custom Trend, Rate, and Counter metrics capture business language.
- abortOnFail needs warm-up delay on long soaks.
- Smoke and full profiles should carry different threshold packs.
k6 thresholds and checks are the two layers that turn a k6 script from a traffic generator into an automated performance gate. Checks validate conditions on individual responses during an iteration. Thresholds evaluate aggregated metrics at the end of the test (and continuously for abort rules) so CI can pass or fail on error rate, latency percentiles, and custom business metrics. In 2026, interviewers and platform teams expect both: checks for functional correctness under load, thresholds for SLO-style criteria.
This guide covers check APIs, rate metrics derived from checks, threshold expressions, tagged thresholds, abortOnFail, custom metrics, and how to avoid flaky gates. For load shapes, see k6 scenarios and executors. For scripting structure, see performance testing with k6 scripts.
TL;DR
| Mechanism | When it runs | Typical question it answers |
|---|---|---|
| check() | Per response / per iteration | Did this status equal 200? |
| Threshold on http_req_failed | End of test (or mid-test abort) | Was failure rate under 1%? |
| Threshold on http_req_duration | Aggregated latency | Was p95 under 500 ms? |
| Threshold on checks | Aggregated check pass rate | Did 99% of checks pass? |
| Custom Trend + threshold | Your business timing | Was checkout step p95 OK? |
Use checks liberally for correctness. Use thresholds as the release contract.
1. Why k6 Thresholds and Checks Matter
Load without assertions is sightseeing. Teams generate charts, then argue in Slack about whether a blip matters. Checks and thresholds encode the argument in code:
- Product says checkout p95 must stay under 1.2s during the marketing spike.
- QA encodes
http_req_durationthresholds and step-level Trends. - CI fails the build when the contract breaks.
Checks also catch "fast but wrong" responses: HTTP 200 with an error payload, or HTTP 302 to a login page when you expected an API JSON body. Performance bugs and functional bugs intertwine under concurrency.
2. Checks: API, Boolean Maps, and Multiple Assertions
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
const res = http.get('https://test.k6.io/');
check(res, {
'status is 200': (r) => r.status === 200,
'body has title': (r) => r.body && r.body.includes('Collection'),
'server header present': (r) => r.headers['Server'] !== undefined,
});
sleep(1);
}
Each property in the map is a named check. k6 records pass/fail rates for each name. Failed checks do not stop the VU by default. The iteration continues unless you also branch on the boolean return value.
const ok = check(res, {
'status is 200': (r) => r.status === 200,
});
if (!ok) {
// optional: skip dependent requests in this iteration
return;
}
Naming matters. Prefer stable, specific names (checkout status 201) over ok or passed. Dashboards and summaries group by check name.
3. Checks on JSON, Timing, and Groups
import http from 'k6/http';
import { check, group } from 'k6';
export default function () {
group('create user', () => {
const res = http.post(
'https://httpbin.test.k6.io/post',
JSON.stringify({ name: 'qa' }),
{ headers: { 'Content-Type': 'application/json' } },
);
check(res, {
'status 200': (r) => r.status === 200,
'json name echoed': (r) => {
try {
return r.json().json.name === 'qa';
} catch (e) {
return false;
}
},
'duration under 800ms': (r) => r.timings.duration < 800,
});
});
}
Notes:
r.json()can throw on non-JSON bodies; guard it.- Per-response duration checks are not a substitute for percentile thresholds. A single-request cap is a hard functional-ish assertion; SLOs almost always need percentiles across the population.
grouphelps organize the script and adds group tags to metrics.
4. Thresholds: Syntax and Built-in Metrics
Thresholds live under options.thresholds. Keys are metric names, optionally with tag filters. Values are arrays of expression strings.
export const options = {
vus: 10,
duration: '2m',
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500', 'p(99)<1000'],
checks: ['rate>0.99'],
},
};
Common built-ins:
| Metric | Type | Useful expressions |
|---|---|---|
| http_req_duration | Trend | p(95)<500, p(99)<1000, avg<300 |
| http_req_failed | Rate | rate<0.01 |
| http_reqs | Counter/Rate | rate>100 (throughput floor) |
| checks | Rate | rate>0.99 |
| iterations | Rate | rate>10 |
| vus | Gauge | value<200 (sanity) |
| data_received | Counter | count<... (rare) |
Expression operators depend on metric type. Trends support p(n), avg, med, min, max, value. Rates support rate. Gauges support value. Counters support count.
When a threshold fails, k6 exits with a non-zero code, which fails CI jobs. That is the primary automation hook for k6 thresholds and checks together.
5. Tagged Thresholds for Multi-Flow Accuracy
Blended p95 across browse and checkout hides regressions. Tag requests and threshold on tags:
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
mixed: {
executor: 'constant-vus',
vus: 20,
duration: '3m',
},
},
thresholds: {
'http_req_duration{name:catalog}': ['p(95)<400'],
'http_req_duration{name:checkout}': ['p(95)<1200'],
'http_req_failed{name:checkout}': ['rate<0.005'],
checks: ['rate>0.99'],
},
};
export default function () {
const catalog = http.get('https://test.k6.io/', { tags: { name: 'catalog' } });
check(catalog, { 'catalog 200': (r) => r.status === 200 });
const checkout = http.get('https://test.k6.io/contacts.php', {
tags: { name: 'checkout' },
});
check(checkout, { 'checkout 200': (r) => r.status === 200 });
}
The name tag is special-cased in k6 for URL grouping when set carefully; you can also use custom tags like flow or api. Be consistent. Threshold keys must match the tag set you emit.
6. abortOnFail and delayAbortEval
For long soaks, you may want to stop early when the system is clearly unhealthy:
export const options = {
thresholds: {
http_req_failed: [
{
threshold: 'rate<0.05',
abortOnFail: true,
delayAbortEval: '1m',
},
],
http_req_duration: ['p(95)<800'],
},
};
delayAbortEval avoids aborting during cold start. Use abort carefully in shared environments so one bad deploy does not strand cleanup jobs; pair with always-run teardown in orchestration outside k6 when needed.
7. Custom Metrics with Thresholds
import http from 'k6/http';
import { check } from 'k6';
import { Counter, Rate, Trend } from 'k6/metrics';
const checkoutTrend = new Trend('checkout_time', true);
const businessErrors = new Counter('business_errors');
const loginSuccess = new Rate('login_success');
export const options = {
vus: 5,
duration: '2m',
thresholds: {
checkout_time: ['p(95)<1000'],
login_success: ['rate>0.99'],
business_errors: ['count<10'],
},
};
export default function () {
const login = http.get('https://test.k6.io/my_messages.php');
const ok = check(login, { 'login page 200': (r) => r.status === 200 });
loginSuccess.add(ok);
const start = Date.now();
const res = http.get('https://test.k6.io/contacts.php');
checkoutTrend.add(Date.now() - start);
if (res.status !== 200) {
businessErrors.add(1);
}
}
Custom metrics express product language: order_create_time, search_empty_rate, payment_timeouts. Thresholds on those metrics make reports readable to non-k6 stakeholders.
8. k6 Thresholds and Checks vs Status Handling
| Concern | Prefer |
|---|---|
| This response must be JSON with id | check |
| Whole test p95 latency SLO | threshold on Trend |
| Fail CI on error budget | threshold on http_req_failed |
| Stop hammering a dead site mid-soak | threshold abortOnFail |
| Count domain errors hidden behind HTTP 200 | custom Rate/Counter + threshold |
| Correlate dynamic tokens | script logic (see correlating dynamic values) |
A common anti-pattern: only checking status codes, never thresholding latency. Another: only thresholding latency, never checking bodies, so you celebrate fast 500s after a misconfigured gateway that returns tiny error pages.
9. CI Integration Patterns
k6 run --out json=summary-raw.json script.js
echo $? # non-zero when thresholds fail
GitHub Actions sketch:
jobs:
perf-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Run smoke performance
run: k6 run -e PROFILE=smoke scripts/load.js
Installation methods vary by year and distro; prefer your org's approved package source. The important contract is: threshold failure fails the job. Store HTML/JSON artifacts for human review. More CI framing sits in add CI to a test framework and API performance testing tutorial.
10. Designing Thresholds That Do Not Flake
Flaky performance gates destroy trust. Practical rules:
- Warm up before evaluating strict percentiles (stages or short constant prelude).
- Separate smoke vs nightly thresholds; PR smoke uses looser latency or only error rate.
- Pin environment capacity; do not gate production SLOs on a shared tiny staging.
- Use enough samples; p99 on 40 requests is noise.
- Tag third-party calls separately so SaaS latency does not fail your app gate blindly.
- Document exclusions (known batch windows) in the test README, not in tribal chat.
const isSmoke = (__ENV.PROFILE || 'smoke') === 'smoke';
export const options = {
thresholds: isSmoke
? {
http_req_failed: ['rate<0.05'],
checks: ['rate>0.95'],
}
: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500', 'p(99)<900'],
checks: ['rate>0.99'],
},
};
11. Checks on WebSocket, Browser, and gRPC Paths
k6 covers more than HTTP. The same philosophy applies:
- Protocol modules return objects you can
check. - Latency trends still need thresholds.
- Browser module timelines use different metric names; threshold on the metrics your script actually emits.
Example gRPC status check pattern (service must exist in your environment):
import grpc from 'k6/net/grpc';
import { check } from 'k6';
const client = new grpc.Client();
// client.load(['definitions'], 'hello.proto');
export default () => {
client.connect('grpcbin.test.k6.io:9001', { plaintext: true });
// const res = client.invoke('package.Service/Method', { name: 'qa' });
// check(res, { 'status OK': (r) => r && r.status === grpc.StatusOK });
client.close();
};
Keep protocol-specific samples aligned with your real protos. Inventing fake service methods in production suites only creates false confidence. The rule for k6 thresholds and checks stays constant: assert correctness per message, aggregate SLOs via thresholds.
12. Reporting: Making Failures Actionable
When CI fails, engineers need:
- Which threshold expression failed
- Which tag or scenario
- Whether checks or transport errors dominated
- A link to time-series (Grafana, cloud run URL)
End-of-test summary already prints threshold OK/FAIL lines. Enhance with handleSummary to write Markdown or JSON for pull request comments:
export function handleSummary(data) {
return {
'summary.json': JSON.stringify(data, null, 2),
};
}
Parse failed thresholds in a small script and post a PR comment. Human-readable failure text prevents the "k6 failed, nobody knows why" pattern.
13. Security and Correctness Under Load
Thresholds often focus on speed. Also gate:
http_req_failedfor 5xx budgets- checks that unauthorized calls still return 401/403
- checks that rate limits return expected 429 behavior when that is required
- counters for unexpected 500 content
Performance tests can discover authorization bugs when parallel users swap tokens incorrectly. Combine with careful test data design so you do not DDoS third parties or violate staging data rules. Performance QA still owns ethical load generation.
14. Comparison: Threshold Strategies by Test Type
| Test type | Checks emphasis | Threshold emphasis |
|---|---|---|
| Smoke | Status + body shape | Error rate only |
| Load | Critical path checks | p95/p99 + error rate |
| Stress | Minimal heavy assertions | Failure rate growth, saturation signals |
| Soak | Memory-leak-sensitive business checks | Stable p95 over hours, error budget |
| Spike | Post-spike recovery checks | Error rate during spike, recovery latency |
Match rigor to purpose. A stress test that aborts on the first p95 breach may never reach the breaking point you wanted to observe. Sometimes you reverse thresholds for exploration (observe only) and re-enable gates for release.
15. Worked End-to-End Script
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend, Rate } from 'k6/metrics';
const pageTrend = new Trend('home_page_time', true);
const okRate = new Rate('home_ok');
export const options = {
scenarios: {
ramp: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '1m', target: 0 },
],
},
},
thresholds: {
http_req_failed: ['rate<0.02'],
home_page_time: ['p(95)<600', 'p(99)<1200'],
home_ok: ['rate>0.98'],
checks: ['rate>0.98'],
},
};
export default function () {
const res = http.get('https://test.k6.io/', { tags: { name: 'home' } });
pageTrend.add(res.timings.duration);
const passed = check(res, {
'status 200': (r) => r.status === 200,
'body not empty': (r) => !!r.body && r.body.length > 0,
});
okRate.add(passed);
sleep(1);
}
Run:
k6 run script.js
Inspect the threshold section of the summary. Intentionally break a threshold (for example p(95)<1) once to confirm CI would fail. Verifying the gate is part of building k6 thresholds and checks into a real quality system. For bottleneck follow-up after a red gate, use finding a performance bottleneck.
16. Team Conventions Checklist
- Every merge-blocking script has at least one threshold.
- Check names are stable and human-readable.
- Critical multi-step flows use custom Trends per step.
- Tag strategy documented (
name,flow,scenario). - Smoke vs full profiles use different threshold packs.
- abortOnFail only on long jobs with warm-up delay.
- Secrets never appear in check failure messages or logs.
- Owners listed for each performance suite.
Conventions keep thresholds from rotting into ignored red builds or rubber-stamp green builds.
17. Mapping SLOs to Threshold Expressions
Start from a written SLO, not from a gut feel:
- Availability-style: 99% of HTTP requests succeed during the test window ->
http_req_failed: ['rate<0.01']. - Latency-style: 95% of checkout API calls under 800 ms -> tagged duration threshold on checkout.
- Quality-style: 99.5% of checks pass ->
checks: ['rate>0.995']. - Throughput-style: sustain at least 80 successful iterations per second -> threshold on
http_reqsor iteration rate with care about what the metric means.
Write the SLO sentence in the script header comment so future editors do not "tune" thresholds to green without product consent. Thresholds are product contracts encoded as code, not knobs for green builds.
18. Handling Partial Failures and Degraded Modes
Modern systems return soft failures: HTTP 200 with {"status":"degraded"}, empty search results under overload, or fallback HTML. Pure status === 200 checks miss those modes.
check(res, {
'status 200': (r) => r.status === 200,
'not degraded flag': (r) => {
try {
const body = r.json();
return body && body.status !== 'degraded';
} catch (e) {
return false;
}
},
});
Decide with product whether degraded mode is a threshold failure or a separate Rate metric watched in Grafana. Document the decision. Silent degradation is how performance incidents hide behind green HTTP codes.
19. Local Troubleshooting Workflow When Thresholds Fail
- Re-run with a tiny VU count to separate functional breakage from scale breakage.
- Enable response body logging on a sample (careful with PII) for failed checks only.
- Compare tagged p95 between scenarios.
- Verify generator health (CPU, network, open files).
- Confirm environment version and config flags match the intended build.
- Only then open APM traces for the slow dependency.
k6 thresholds and checks tell you that the contract failed. They do not replace distributed tracing. Use them as the gate, then switch tools for root cause as in finding a performance bottleneck.
20. Example Failure Messages and How to Read Them
When k6 prints threshold failures, read the metric name, the tag set, and the actual versus expected bound. A failure on http_req_duration{name:checkout} with p(95) above the limit means checkout is slow even if catalog is fine. A failure on checks with rate 0.93 means roughly seven percent of assertions failed; open the checks section to see which named check dragged the rate down.
If http_req_failed is red while checks look green, your checks may not cover transport failures, or you only asserted on a subset of requests. Align check placement with every critical call. If the reverse happens (checks red, http_req_failed green), you are getting HTTP success with wrong bodies. That pattern often reveals wrong environment URLs or feature flags.
Practice decoding one intentionally broken run per suite when you first adopt k6 thresholds and checks. The team should share a short "how to read the summary" note next to the script.
Interview Questions and Answers
Q: Difference between a check and a threshold in k6?
A check asserts conditions on a single sample during execution and contributes to the checks metric. A threshold evaluates aggregated metrics against expressions to decide pass/fail for the test run.
Q: Do failed checks fail the test automatically?
No. Failed checks lower the checks pass rate. You typically add a threshold like checks: ['rate>0.99'] or threshold other metrics to fail the process.
Q: How do you threshold p95 latency in k6?
Use a Trend metric such as http_req_duration with an expression like p(95)<500 in options.thresholds.
Q: What is abortOnFail?
A threshold option that stops the test early when the expression fails, optionally after delayAbortEval, useful for long soaks when the system is already outside bounds.
Q: Why tag metrics before thresholding?
Tags let you apply different SLOs to different endpoints or flows instead of one blended percentile that hides regressions.
Q: When do you use custom metrics?
When built-ins do not capture business steps, domain error rates, or multi-request timing that you want named in reports and gates.
Q: How do checks and thresholds work together in CI?
Checks ensure responses are correct under load; thresholds enforce SLO-style aggregates and set the process exit code that CI consumes.
Common Mistakes
- Using only checks with no thresholds, so CI never fails.
- Thresholding p99 on tiny sample sizes.
- Blended latency thresholds across unlike endpoints.
- Asserting per-request duration as if it were a percentile SLO.
- Forgetting that failed checks do not abort VUs by default.
- Copying production SLO numbers onto undersized staging.
- Leaving abortOnFail on without warm-up delay.
- Unstable check names that break dashboards every refactor.
- Ignoring http_req_failed while celebrating fast responses.
- Not verifying that a deliberately broken threshold fails CI.
- Logging sensitive bodies inside check messages.
- Over-strict smoke thresholds that flake every cold start.
Conclusion
k6 thresholds and checks give you correctness under concurrency and automated SLO gates in one tool. Write precise checks for response truth, emit tags and custom metrics for business steps, and attach thresholds that match smoke versus full load profiles. Wire exit codes into CI so performance regressions fail as loudly as unit test regressions.
When a threshold trips, treat it as a defect signal: isolate the flow tag, inspect saturation, and follow a structured bottleneck analysis. Your next upgrade is tighter scenario design so the traffic that feeds those metrics matches real risk.
Interview Questions and Answers
Explain k6 thresholds versus checks.
Checks validate conditions on each sample and build a pass rate. Thresholds evaluate metrics after aggregation to decide if the test meets SLOs. CI should key off thresholds, often including a minimum checks rate.
How do you encode a latency SLO in k6?
Use options.thresholds on http_req_duration or a business Trend with percentile expressions such as p(95)<500 and p(99)<1000, ideally tagged per critical endpoint.
What happens when a check fails?
k6 records a failed check and continues the iteration unless the script branches on the boolean return. The process fails only if thresholds say so.
How do custom metrics support better gates?
Trends, Rates, and Counters capture step timing and domain errors that built-in HTTP metrics may miss, letting thresholds speak product language.
How would you stop a soak when error rate explodes?
Configure an http_req_failed threshold with abortOnFail true and a delayAbortEval window so warm-up does not trigger early abort.
How do you keep thresholds trustworthy?
Warm up, ensure sample size, separate smoke from nightly rigor, tag third parties separately, and verify deliberately broken thresholds fail the pipeline.
Why tag thresholds in multi-scenario tests?
Because blended percentiles hide which flow regressed. Tagged thresholds isolate browse versus checkout SLOs and speed up triage.
Frequently Asked Questions
What are k6 thresholds and checks?
Checks are boolean assertions on individual samples during a test. Thresholds are pass/fail rules on aggregated metrics such as p95 latency or error rate that determine the test exit code.
How do I fail a k6 test if p95 is too high?
Add a threshold on http_req_duration or a custom Trend, for example p(95)<500. When the expression fails, k6 exits non-zero and CI can mark the job failed.
Do I need both checks and thresholds?
Yes for serious suites. Checks catch wrong responses that are still fast. Thresholds enforce statistical SLOs and error budgets across the run.
What does checks: ['rate>0.99'] mean?
At least 99 percent of all check() evaluations must pass. It converts individual check results into a CI-enforceable aggregate.
How can I apply different SLOs to different endpoints?
Tag requests (for example name or flow tags) and write threshold keys with tag filters such as http_req_duration{name:checkout}.
When should I use abortOnFail?
On long soaks or stress runs where continuing after a severe error budget breach wastes time and may harm a shared environment. Add delayAbortEval for warm-up.
Why are my performance thresholds flaky in CI?
Common causes include cold starts, undersized environments, too few samples for percentiles, noisy neighbors, and smoke profiles that reuse production-strict latency gates.