Skip to main content
Sep 21, 2026api slo error budget

By Lucas Yoris · Performate

SLA, SLO, and Error Budgets for APIs: Turning Metrics into Release Gates

Map customer-facing SLIs to k6 thresholds, spend error budgets deliberately, and block releases when burn violates policy.

Your dashboard shows 99.2% availability this month—green across the board. Then checkout p99 doubles for six hours on Tuesday, support tickets spike, and finance asks why you shipped a feature release the same morning. The gap is not missing metrics; it is missing policy. SLAs describe what you owe customers, SLOs define what you promise internally, and error budgets translate those promises into a finite allowance of bad minutes you can spend on velocity.

API teams that treat SLOs as wall charts instead of release gates learn this the hard way. k6 makes the policy executable: thresholds on http_req_duration, custom business checks, and tagged routes turn abstract targets into pass/fail signals CI and staging can enforce. In this guide you will learn which SLIs users actually feel, how to map them to k6 thresholds, and when a burning error budget should freeze launches—not just trigger a Slack emoji.

Why SLIs matter more than infrastructure charts

Customers do not experience CPU utilization. They experience slow checkout, failed payments, and timeouts on search. Pick SLIs that mirror those moments:

  • Latency tails on critical routes—checkout, auth, search—not global averages (p95 vs p99).
  • Availability measured at the HTTP layer your clients hit, not only pod readiness.
  • Correctness signals where duplicates or partial writes have financial impact (order IDs, idempotency keys).

Think of an error budget like a monthly fuel allowance. Spend it on planned experiments and known trade-offs; when the tank is empty, you stop adding features and fix reliability debt (baseline regression).

When green dashboards still violate SLO

Synthetic uptime checks can pass while real user journeys fail under concurrency. Pair production SLIs with load tests that exercise the same routes at realistic rates—otherwise budgets reflect monitoring gaps, not user experience.

Practical k6 implementation: SLI thresholds and budget burn

Define one SLO per critical journey, then encode it as k6 thresholds tagged by route. Staging runs should use stricter gates than production targets so you catch regressions before they consume live budget.

Example script (illustrative—not production-ready). Fictional URLs, tokens, and SLO numbers—adapt to your environment.

What this example demonstrates:

  • Route-scoped SLIs: thresholds on http_req_duration{route:checkout} and {route:search} instead of one global line.
  • Availability gate: http_req_failed capped at 0.5% for the run—proxy for monthly error budget burn in CI.
  • Custom business SLI: a Rate metric counting duplicate order responses, surfaced as a threshold.
  • Build tagging: git_sha env var tags every request so release comparisons stay apples-to-apples.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const BASE = __ENV.API_BASE || 'https://staging.example.com';
const duplicateOrders = new Rate('duplicate_order_responses');

export const options = {
  scenarios: {
    checkout_sli: {
      executor: 'constant-arrival-rate',
      rate: 20,
      timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 15,
      maxVUs: 60,
      tags: { route: 'checkout', slo: 'checkout-latency' },
      exec: 'checkoutFlow',
    },
    search_sli: {
      executor: 'constant-arrival-rate',
      rate: 50,
      timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 10,
      maxVUs: 40,
      tags: { route: 'search', slo: 'search-latency' },
      exec: 'searchFlow',
    },
  },
  thresholds: {
    // Internal SLO: 99% of checkout calls < 800 ms (staging gate uses p95)
    'http_req_duration{route:checkout}': ['p(95)<800', 'p(99)<1200'],
    'http_req_duration{route:search}': ['p(95)<400', 'p(99)<700'],
    http_req_failed: ['rate<0.005'],
    duplicate_order_responses: ['rate<0.001'],
  },
};

export function checkoutFlow() {
  const idempotencyKey = `ord-${__VU}-${__ITER}-${Date.now()}`;
  const body = JSON.stringify({ sku: 'SKU-100', qty: 1 });
  const res = http.post(`${BASE}/v1/checkout`, body, {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${__ENV.TOKEN}`,
      'Idempotency-Key': idempotencyKey,
    },
    tags: { route: 'checkout', git_sha: __ENV.GIT_SHA || 'local' },
  });
  check(res, { 'checkout 2xx': (r) => r.status >= 200 && r.status < 300 });
  duplicateOrders.add(res.headers['X-Duplicate-Order'] === 'true');
  sleep(0.2);
}

export function searchFlow() {
  const res = http.get(`${BASE}/v1/search?q=laptop&limit=20`, {
    headers: { Authorization: `Bearer ${__ENV.TOKEN}` },
    tags: { route: 'search', git_sha: __ENV.GIT_SHA || 'local' },
  });
  check(res, { 'search 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.1);
}

Patterns that work

  • Staged gates: smoke in CI (low rate, strict errors) + weekly soak slices at production-shaped load (CI/CD load testing).
  • Thresholds per route aligned to product SLO docs—not one number copied from a marketing SLA PDF.
  • Documented budget resets: if you widen a threshold after a failed run, record stakeholder sign-off and the new baseline date.

Anti-patterns to avoid

  • Vanity budgets that always pass because thresholds were silently loosened after red builds.
  • Measuring only http_req_duration without failure rate—latency and availability burn budget together.
  • Using production SLO numbers directly in aggressive load tests; staging gates should be tighter.

Pro tip (example command):

k6 run slo-gates.js -e GIT_SHA=$(git rev-parse --short HEAD) --summary-export=slo-summary.json

What this command demonstrates: every request carries the build tag, and the JSON export feeds release dashboards or error-budget burn calculators outside k6.

Decision framework: when to ship vs freeze

SituationRecommended action
Budget >50% remaining, staging gates greenShip planned features; monitor burn daily
Budget 20–50%, staging p99 elevatedShip bugfixes only; no new dependencies
Budget <20% or burn rate acceleratingFeature freeze; perf/reliability sprint
CI smoke fails on http_req_failedBlock merge until root cause fixed
Threshold widened without sign-offTreat as policy violation; revert or document

Ship if staging load tests pass at current traffic shape and remaining budget covers expected launch risk.

Freeze if error burn in production or staging exceeds policy for two consecutive measurement windows—velocity stops until SLIs recover.

Reset baselines only with written approval, a new target date, and archived k6 summaries for comparison (k6 thresholds examples).

Observability, documentation, and next steps

SLO policy only works when teams share the same definitions:

  • Publish SLI definitions (route, percentile, window) where eng and product can find them.
  • Wire k6 threshold failures to the same channel as production budget alerts.
  • Archive summary-export JSON per release with git_sha for audit trails.
  • Review burn weekly in a 15-minute ritual—no slide deck, just numbers vs policy.
  • Pair SLO gates with how to read load test reports so non-specialists interpret failures.

How Performate simplifies SLO-driven load testing

Translating SLO spreadsheets into repeatable k6 runs breaks down when every team edits scripts differently. Below is a concrete workflow for checkout and search SLIs—adapt routes and numbers to your policy docs.

Example: encode checkout and search SLOs without threshold drift

  1. Import a Postman collection covering login, checkout POST, and search GET—the same journeys your SLI doc names. Problem solved: one artifact matches product language and test code.
  2. Create two scenarios in the visual editor—checkout_sli at 20 req/s and search_sli at 50 req/s—with tags route:checkout and route:search. Problem solved: route-scoped metrics without hand-copying executor blocks.
  3. Set thresholds in the UI to mirror staging gates: checkout p95 < 800 ms, search p95 < 400 ms, http_req_failed < 0.5%. Problem solved: QA and SRE edit the same numbers product approved.
  4. Run and open the comparison view after each release candidate; filter by route tag and check tail latency before merge. Problem solved: release debates reference one export, not forked spreadsheets.
  5. Export the k6 script with env vars for GIT_SHA and wire it into CI smoke (CI/CD load testing). Problem solved: desktop tuning and pipeline gates stay aligned.
  6. Archive run history beside deployment dates so error-budget postmortems map to exact scenario configs.

That workflow matches this post's cta: ship performance confidence faster when imports, thresholds, and reports live in one desktop workspace.

Closing takeaway

Error budgets are not abstract SRE vocabulary—they are release policy with a number attached. Map each customer-facing SLI to a tagged k6 threshold, block merges when staging burn violates gates, and freeze features when live budget runs dry.

Run this week's staging mix against your checkout and search SLIs before the next launch review—and note whether remaining budget actually covers the risk you are about to ship.

Try Performate free | Book a demo | k6 thresholds

Ready to optimize your API performance?

Explore how Performate simplifies k6 load testing—from imports to results—so your team ships performance confidence faster.

← Back to all posts