← Recipes

阅读中文版本

Enforce an API Latency Budget as a CI Gate with k6

Encode percentile thresholds in a k6 scenario, run it against a staged build in CI, and fail the merge when p95 or error rate breaches budget.

~35 mintestingperformanceci

Task

Turn latency requirements into executable thresholds that block regressions at pull-request time.

When to use this

Latency regressed 40 percent over six releases because nothing measured it. Dashboards showed the trend after the fact; users lived it first.

Prerequisites

  • k6 installed locally and in CI
  • A disposable environment running the candidate build
  • An agreed latency budget per endpoint class

Steps

1. Write the scenario with thresholds as code.

import http from "k6/http";
import { group } from "k6";

export const options = {
  stages: [
    { duration: "30s", target: 20 },
    { duration: "1m", target: 20 },
  ],
  thresholds: {
    "http_req_duration{endpoint:search}": ["p(95)<300"],
    "http_req_duration{endpoint:detail}": ["p(95)<120"],
    "http_req_failed": ["rate<0.01"],
  },
};

Expected output: The thresholds are part of the script: search p95 under 300 ms, detail under 120 ms, errors under one percent.

2. Tag requests by endpoint class.

group("search", () => {
  http.get(`${__ENV.BASE}/search?q=k6`, { tags: { endpoint: "search" } });
});
group("detail", () => {
  http.get(`${__ENV.BASE}/items/42`, { tags: { endpoint: "detail" } });
});

Expected output: Thresholds evaluate per tag, so a slow search cannot hide inside a fast average.

3. Run it against the candidate build in CI.

- name: Latency budget gate
  run: |
    k6 run --out json=results.json tests/load/api-budget.js
  env:
    BASE: http://staging-candidate:3000

Expected output: The job exits non-zero the moment any threshold breaches, failing the pull request.

4. Inspect the failing threshold in the summary.

k6 run tests/load/api-budget.js 2>&1 | grep -A4 "http_req_duration"

Expected output: The summary prints each threshold with its value and pass/fail mark, naming the endpoint that regressed.

5. Tune load to keep the gate fast.

stages: [
  { duration: "15s", target: 10 },
  { duration: "30s", target: 10 },
]

Expected output: A sub-minute gate catches regressions while staying cheap enough to run on every pull request.

Troubleshooting

Problem

Flaky thresholds on shared staging

Solution

Other traffic pollutes measurements; run against a dedicated ephemeral environment per pull request, or add a warmup stage and accept wider budgets.

Problem

Cold caches fail the gate on first runs

Solution

Add a warmup stage before measurement, or run data warmers in the environment setup step.

Problem

Gate too slow for every PR

Solution

Run the fast smoke profile per PR and the full budget profile on merge to main; both share the same thresholds object.

Limitations

  • Thresholds measure the staged environment, not production topology; keep budgets conservative relative to production targets.
  • k6 measures HTTP behavior; queue-based async endpoints need scenario-specific probes.

A budget that cannot be ignored

Latency budgets written in documents decay; latency budgets written as thresholds execute. k6 evaluates percentile thresholds per tagged endpoint, and a breach exits non-zero, which turns a slow pull request into a failed check instead of a trend line discovered next quarter. The script is the requirement, the CI run is the audit, and the failure names the endpoint.

export const options = {
  thresholds: {
    "http_req_duration{endpoint:search}": ["p(95)<300"],
    "http_req_duration{endpoint:detail}": ["p(95)<120"],
    "http_req_failed": ["rate<0.01"],
  },
};

Percentiles per endpoint, not averages overall

A single average threshold lets a 3-second search hide behind 50-millisecond detail views. Tagging requests by endpoint class makes each threshold answer a specific question, and the failure message points at the exact class that regressed. Budgets become readable: search is a 300 ms product, detail is a 120 ms product.

Keep the gate proportional

A gate that doubles CI time gets disabled within a month. Size the load profile for signal, not simulation: modest concurrency, short stages, and budgets calibrated against a known-good baseline. The full storm belongs in scheduled nightly runs; the pull request gate needs only enough load to expose a regression beyond noise.