Shape load with k6 scenarios and gate CI on SLO thresholds
Use ramping-vus stages and http_req_duration / http_req_failed thresholds to make k6 load tests repeatable and CI-gateable without manual inspection.
13 Jan 2026, 19:32 UTC

Problem: manual load checks break CI
Load tests that use a fixed number of virtual users give a single point-in-time number and force manual inspection. The useful takeaway is to shape load with staged ramps and decide pass or fail automatically from SLO-based thresholds on latency and error rate.
Desired outcome
A repeatable test that ramps up gradually, sustains a steady state, then ramps down. Pass or fail is driven by thresholds on http_req_duration and http_req_failed rather than eyeballing a summary.
Prerequisites
- A working k6 binary on the machine or CI runner. Version behavior for scenarios and executors is version sensitive.
- A test script with an HTTP request in the default function. The default function is executed repeatedly by each VU.
- Access to a test environment that can be safely loaded. Do not run against production.
- A CI step that can run k6 and interpret the process exit code. k6 exits non-zero when thresholds are breached.
Focused procedure
Script skeleton with scenarios and thresholds
Keep the default function lightweight. Define load shape in the scenarios object and express SLOs in thresholds.
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
scenarios: {
ramp_profile: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 20 },
{ duration: '5m', target: 20 },
{ duration: '2m', target: 0 }
],
gracefulRampDown: '30s'
}
},
thresholds: {
'http_req_duration': ['p(95)<500'],
'http_req_failed': ['rate<0.01']
}
};
export default function () {
const res = http.get('{{TARGET_URL}}');
sleep(1);
}
executor ramping-vus controls VU count over time. stages define target VUs and duration for each phase. thresholds express SLOs for p95 latency and error rate. Thresholds are evaluated after the test completes, not during.
Run locally then in CI
Run locally first with a small profile to confirm metrics appear.
k6 run --out json=results.json script.js
Run from a terminal where k6 is installed and the runner has network access to {{TARGET_URL}}. No elevated permissions are required for a read-only load test.
In CI, invoke the same command and gate on exit code. A non-zero exit means thresholds failed.
Expected checks
- Threshold evaluation summary shows pass or fail for http_req_duration and http_req_failed.
- Key metrics: http_reqs total, http_req_duration percentiles, VUs over time, iteration rate over time.
- Exit code zero for pass, non-zero for failure when thresholds are breached.
- Compare VU and iteration rate against the defined stages to confirm load shaping matches intent.
Recovery options and limitations
If the system saturates, reduce target VUs or stage duration. If cold start biases results, add a warm-up stage before the measured steady state.
Local machine resources can limit achievable VUs, leading to inaccurate results if host capacity is exceeded. Shared test environments can cause interference and flaky metrics.
Relaxing thresholds temporarily is possible but should be documented as an exception. Splitting tests into smoke and soak scenarios isolates short validation from longer endurance checks.
Verification steps: run with a small stage profile and confirm summary output, introduce an intentional threshold breach and verify non-zero exit and failed summary, and run the same script in CI to confirm pipeline status reflects pass or fail.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.