---
title: "k6 Thresholds Examples: Alert on the Metrics That Matter"
description: "Concrete k6 threshold patterns for latency percentiles, error rates, and custom metrics—plus how exit codes turn runs into CI gates."
publishDate: "2026-05-26"
draft: false
keyword: "k6 thresholds examples"
intent: "MOFU"
cta: "Define and compare threshold outcomes faster with Performate scenario workflows."
tags: ["k6", "thresholds", "slo", "api-performance"]
---

A load test that "looks fine" in a slide deck but never fails the build is decoration. Thresholds turn k6 metrics into **release gates**: violated thresholds set a non-zero exit code CI can enforce ([thresholds documentation](https://k6.io/docs/using-k6/thresholds/)).

The art is choosing **few, meaningful** thresholds—segmented by route and percentile—not copying every metric k6 emits. In this guide you will learn patterns for latency percentiles, error rates, custom Trends, and tagged SLOs that match how product talks about reliability.

## Why thresholds beat eyeballing summaries

Checks validate individual responses; thresholds aggregate **run-level SLOs**:

- **`http_req_duration`** supports `p(95)`, `p(99)`, `avg`, and `max` selectors ([HTTP metrics](https://k6.io/docs/using-k6/metrics/)).
- **Tagged thresholds** like `http_req_duration{route:checkout}` isolate critical paths.
- **`http_req_failed`** catches transport and HTTP failures—pair with [error taxonomy](/en/blog/load-test-error-taxonomy) when tuning.
- **Custom metrics** (`Trend`, `Rate`, `Counter`) threshold business steps—payment authorize, cache miss rate.

Without thresholds, teams debate screenshots. With them, merges fail objectively—when configured honestly.

### When aggregate thresholds hide checkout pain

Global `p(95)<500` passes while checkout `p(99)` doubles. Align gates with [p95 vs p99 guidance](/en/blog/p95-vs-p99-latency) and [observability tags](/en/blog/k6-observability-metrics-tags). When you run [multi-version traffic mixes](/en/blog/api-versioning-load-testing), duplicate that pattern per version tag so a fast v2 path cannot mask a regressing v1 tail.

Pair thresholds with [k6 scenario types](/en/blog/k6-scenario-types-explained): smoke runs deserve strict error gates; stress runs may document expected SLO breaches instead of failing the build on every percentile.

## Practical k6 implementation: layered thresholds

Layer global safety rails, route-specific latency, and custom business metrics.

**Example script (illustrative—not a production-ready test).** Fictional SLO numbers.

**What this example demonstrates:**

- **Global error ceiling:** `http_req_failed` rate gate for any release.
- **Route-specific latency:** checkout tighter than catalog browse.
- **Custom Trend threshold:** `payment_authorize_duration` for downstream PSP step.
- **Abort on critical failure:** `abortOnFail` on checkout error rate for fast CI feedback.

```javascript
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';

const paymentAuthorize = new Trend('payment_authorize_duration', true);
const BASE = __ENV.API_BASE || 'https://staging.example.com';

export const options = {
  scenarios: {
    checkout_path: {
      executor: 'constant-arrival-rate',
      rate: 10,
      timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 15,
      maxVUs: 60,
      tags: { suite: 'threshold-demo' },
      exec: 'checkoutJourney',
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    'http_req_failed{route:checkout}': [{ threshold: 'rate<0.005', abortOnFail: true, delayAbortEval: '30s' }],
    'http_req_duration{route:catalog}': ['p(95)<400', 'p(99)<700'],
    'http_req_duration{route:checkout}': ['p(95)<850', 'p(99)<1300'],
    payment_authorize_duration: ['p(95)<2000'],
  },
};

export function checkoutJourney() {
  let res = http.get(`${BASE}/catalog`, { tags: { route: 'catalog' } });
  check(res, { 'catalog 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.3);

  res = http.post(`${BASE}/checkout`, JSON.stringify({ sku: 'SKU-1', qty: 1 }), {
    headers: { 'Content-Type': 'application/json' },
    tags: { route: 'checkout' },
  });
  check(res, { 'checkout 2xx': (r) => r.status >= 200 && r.status < 300 });

  const start = Date.now();
  res = http.post(`${BASE}/payments/authorize`, JSON.stringify({ amount: 99 }), {
    headers: { 'Content-Type': 'application/json' },
    tags: { route: 'payment' },
  });
  paymentAuthorize.add(Date.now() - start);
  check(res, { 'payment 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.5);
}
```

**Patterns that work**

- **CI smoke:** strict `http_req_failed`, relaxed duration on non-critical routes.
- **Pre-release:** full percentile stack on revenue paths only.
- **`abortOnFail` sparingly:** on catastrophic error rates, not on first p99 blip in short runs.
- **Thresholds as code in Git** reviewed like application changes.

**Anti-patterns to avoid**

- Thresholds on every emitted metric—noise drowns signal.
- Copying prod SLO numbers into 2-minute smokes with insufficient samples.
- Ignoring `thresholds` on custom metrics you never defined in script.

**Pro tip (example command):** fail CI explicitly on threshold breach.

```bash
k6 run checkout-thresholds.js; echo "exit code: $?"
```

**What this command demonstrates:** k6 exits non-zero on threshold failure—wire this directly into CI steps ([load testing in CI/CD](/en/blog/load-testing-in-ci-cd)).

## Decision framework: which thresholds per gate type

| Gate type | Recommended thresholds |
|:---|:---|
| PR smoke (2–3 min) | `http_req_failed`; one critical route `p(95)` |
| Nightly regression | Full route set `p(95)` + `p(99)` on checkout |
| Pre-prod capacity | Error rate + custom Trends + dropped_iterations |
| Third-party dependency | Separate tag `{dependency:payments}` latency/error |
| Post-deploy canary | Tight `http_req_failed` + tail latency on canary tag |

**Gate on p95 if** you need stable CI signal on short runs.

**Gate on p99 if** payment or safety paths tolerate rare slowness poorly.

**Gate on custom Trends if** business steps span multiple HTTP calls.

## Observability, documentation, and next steps

- [ ] Document which thresholds map to published SLOs vs internal budgets.
- [ ] Store threshold config in version control beside scenario JSON.
- [ ] Review false positives monthly—tighten or lengthen runs, do not disable blindly.
- [ ] Correlate threshold breaches with [error taxonomy](/en/blog/load-test-error-taxonomy) buckets.
- [ ] Archive summary JSON when thresholds fail for diffable regressions.

## How Performate simplifies threshold workflows

**Example: checkout SLO gates without syntax hunting**

1. **Import checkout and payment requests.** *Problem solved:* routes exist as named operations for tagged thresholds.
2. **Set thresholds in the visual panel**—catalog `p(95)<400`, checkout `p(95)<850`, global error `<1%`. *Problem solved:* PM-readable SLO lines export to valid k6 syntax.
3. **Run 5-minute arrival-rate scenario.** *Problem solved:* see pass/fail per threshold in integrated report, not only terminal output.
4. **Compare last three runs** for p99 drift on checkout. *Problem solved:* regression debates use one dashboard.
5. **Enable abort-on-fail for checkout errors** in UI for CI exports. *Problem solved:* fast pipeline feedback without custom wrapper scripts.
6. **Export k6 with thresholds embedded** for GitOps. *Problem solved:* desktop tuning matches CI gates exactly.

## Closing takeaway

Thresholds are **contracts with your pipeline**—keep them few, tagged, and tied to real SLO language. Gate errors everywhere; gate latency where the business feels pain; use custom Trends when HTTP averages lie.

Pick one critical route this week, replace a vague "should be fast" note with `p(95)` and `p(99)` thresholds, and wire k6's exit code into CI.

[Try Performate free](https://performate.app) | [Book a demo](/demo) | [k6 thresholds](https://k6.io/docs/using-k6/thresholds/)
