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_failedcapped at 0.5% for the run—proxy for monthly error budget burn in CI. - Custom business SLI: a
Ratemetric counting duplicate order responses, surfaced as a threshold. - Build tagging:
git_shaenv 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_durationwithout 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
| Situation | Recommended action |
|---|---|
| Budget >50% remaining, staging gates green | Ship planned features; monitor burn daily |
| Budget 20–50%, staging p99 elevated | Ship bugfixes only; no new dependencies |
| Budget <20% or burn rate accelerating | Feature freeze; perf/reliability sprint |
CI smoke fails on http_req_failed | Block merge until root cause fixed |
| Threshold widened without sign-off | Treat 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-exportJSON per release withgit_shafor 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
- 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.
- Create two scenarios in the visual editor—
checkout_sliat 20 req/s andsearch_sliat 50 req/s—with tagsroute:checkoutandroute:search. Problem solved: route-scoped metrics without hand-copying executor blocks. - Set thresholds in the UI to mirror staging gates: checkout
p95 < 800 ms, searchp95 < 400 ms,http_req_failed < 0.5%. Problem solved: QA and SRE edit the same numbers product approved. - 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.
- Export the k6 script with env vars for
GIT_SHAand wire it into CI smoke (CI/CD load testing). Problem solved: desktop tuning and pipeline gates stay aligned. - 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.
Ready to optimize your API performance?
Explore how Performate simplifies k6 load testing—from imports to results—so your team ships performance confidence faster.