CI/CD8 min read4 августа 2026 г.

SLO Gates with Thresholds, Step by Step

Turn load-test metrics into pass/fail SLO gates: k6-style threshold expressions, fail/warn/info severities, the thresholds field in the run summary JSON, and CI pipelines that go red when p95 blows its budget.

Автор: Perfscale Team

What you'll build

A load test that doesn't just measure latency but enforces it: a run-level SLO gate that evaluates k6-style expressions (p95<500, rate<0.05) against the metrics the whole run collected, and exits non-zero when the budget is blown — so CI goes red instead of merging a regression.

We'll go in three steps: a baseline HTTP test, an SLO gate in the config's after: block, and the same gate wired into CI. Everything here is free and open sourcestd/thresholds@v1 ships in the perfscale engine on every plan (v0.8.1+).

Prerequisites

  • The perfscale CLI (install instructions): npm install -g @perfscale/exe, or perfscale self-update to be current (thresholds need v0.8.1+ — check with perfscale --version)
  • Any HTTP endpoint to test. A local python3 -m http.server 8080 in some static directory is enough to follow along.

Step 1 — A baseline run

Thresholds gate on run metrics, so first you need a run that emits some. Create test.yaml:

steps:
  - name: get index
    uses: std/http@v1
    with:
      url: http://127.0.0.1:8080/
      method: GET
    check:
      status: 200

And config.yaml:

vus: 10
duration: 30s

Run it:

perfscale run -f test.yaml -c config.yaml

The end-of-run summary prints the metric families you'll gate on: http_req_duration (a sample histogram with avg/p50/p95/p99/max), http_req_failed (a failure-rate metric the runner derives automatically — every step invocation records a 0/1 sample under <family>_failed), plus counters like iterations. Note the names in your summary — a gate can only reference metrics the run actually collected.

One thing to know before moving on: inline check: failures are load-test feedback — they're printed and counted, but the run still exits 0. Thresholds are how you turn those numbers into a hard pass/fail.


Step 2 — Add the SLO gate

Gates live in the config file's after: block — they evaluate once, after every VU has stopped, over the same HDR histograms the summary prints. Gate numbers match summary numbers, always.

Replace config.yaml:

vus: 10
duration: 30s

after:
  - name: slo gate
    use: std/thresholds@v1
    with:
      http_req_duration: ["p95<500", "avg<300", "max<2000"]
      http_req_failed: ["rate<0.01"]
    severity: fail
    message: "checkout SLO"

Run again. If every expression holds, the gate logs PASS and the run exits 0. If one is violated you'll get a summary line like http_req_duration p95=612ms ≥ 500ms; checkout SLO on stderr — and, because severity: fail, the run exits non-zero.

The moving parts:

  • Expressions are <agg><op><number>: agg ∈ avg, min, max, p50, p90, p95, p99, count, rate; op ∈ <, <=, >, >=, ==, !=; the number is a plain float, no units. Whitespace is tolerated — p95 < 500 parses fine.
  • Which aggregates apply depends on the metric kind. Sample metrics (http_req_duration, grpc_msg_rtt, ws_msg_rtt, db_query_duration, …) take the percentile/avg/min/max aggs; count is the number of samples. Counter metrics take only count (final value). Failure metrics (http_req_failed, …) take rate (failed/total in 0.0..=1.0) and count.
  • severity decides what a violation becomes: fail (default) exits non-zero; warn and info print but exit zero — useful for soft budgets you're still tuning.
  • message is an optional label (interpolated) appended to the violation summary — put the SLO name there so a red log line tells you which contract broke.

Several gates compose: add more std/thresholds@v1 steps (e.g. a strict fail gate for hard SLOs, a warn gate for aspirational ones) — the worst status wins.

Step 3 — Machine-readable results and CI

Each gate's output (available via outputs / __last__) is JSON:

{ "status": "fail",
  "message": "http_req_duration p95=612ms ≥ 500ms; checkout SLO",
  "violations": [{ "metric": "http_req_duration", "expr": "p95<500", "actual": 612.0 }] }

The same shape lands in the run summary JSON (perfscale run --summary-export summary.json) under a thresholds field, so dashboards and pipeline annotations can read gate results without parsing stdout.

Gating CI is then just "run the test": on GitHub, the Perfscale/github-action step installs the pinned CLI and runs your file + config; on GitLab, include: the templates from Perfscale/gitlab-ci. A violated fail gate means a non-zero exit, which means a red job — no wrapper scripts, no grep over logs. The full wiring (REST API triggers, webhooks back into CI) is in CI/CD integration.

Errors are loud, not silent

A broken gate never silently passes CI. An unknown metric name, an empty with:, an unparseable expression, or an aggregate that doesn't apply to the metric kind — all of these fail the gate and the run with an error listing the metrics that are present. If a gate is green, the metric existed, the expression parsed, and the number held.

Next steps

Комментарии

Ответить в Bluesky