Your first k6 script
A 20-line script that hits /healthz with 10 VUs.
Time to write a real k6 test. We'll hit your local Grit API's /healthz endpoint with 10 virtual users for 30 seconds, then read the output.
The script
import http from 'k6/http'import { check, sleep } from 'k6'// Configurationexport const options = {vus: 10, // virtual usersduration: '30s', // how longthresholds: {http_req_duration: ['p(95)<200'], // 95% of requests under 200mshttp_req_failed: ['rate<0.01'], // <1% errors},}// The test function runs in a loop for each VUexport default function () {const res = http.get('http://localhost:8080/healthz')check(res, {'status is 200': (r) => r.status === 200,'body has "ok"': (r) => r.body.includes('ok'),})sleep(1) // pause 1s between iterations per VU}
Three sections, each non-negotiable:
options— how many VUs, for how long, with what thresholds.- The default function — what each VU does repeatedly until time runs out.
check— assertions on the response. Failed checks count towardchecksmetric.
Run it
# Make sure your API is runninggo run apps/api/cmd/server/main.go# In another terminalk6 run tests/k6/smoke.js
Reading the output
checks.........................: 100.00% ✓ 600 ✗ 0data_received..................: 240 kB 8.0 kB/sdata_sent......................: 60 kB 2.0 kB/shttp_req_blocked...............: avg=15µs p(95)=24µshttp_req_connecting............: avg=4µs p(95)=0shttp_req_duration..............: avg=12ms p(95)=19ms ← key metric{ expected_response:true }...: avg=12ms p(95)=19mshttp_req_failed................: 0.00% ✓ 0 ✗ 300http_req_receiving.............: avg=80µs p(95)=120µshttp_req_sending...............: avg=22µs p(95)=39µshttp_req_tls_handshaking.......: avg=0s p(95)=0shttp_req_waiting...............: avg=11ms p(95)=18mshttp_reqs......................: 300 10/s ← throughputiteration_duration.............: avg=1.01s p(95)=1.02siterations.....................: 300 10/svus............................: 10vus_max........................: 10
Four metrics matter on a smoke test:
checks— 100% means every response was healthy.http_req_duration— latency. The p(95) tells you the 95th-percentile latency — the bar most teams set SLOs against.http_req_failed— error rate. Anything > 0% on a healthy smoke test is a red flag.http_reqs— total + rate. Useful for capacity planning.
Threshold pass / fail
At the bottom of the output:
✓ http_req_duration..p(95)<200✓ http_req_failed....rate<0.01
✓ means the threshold held. ✗ means a breach — and k6 exits with non-zero status. That's how CI knows your PR regressed the API.
VUs vs. iterations — the mental model
VUs are virtual users — independent loops running your default function. With 10 VUs, ten copies of the function are running concurrently. Each VU loops until the duration ends.
An iteration is one trip through the default function. With sleep(1) and a 30s duration, each VU does about 30 iterations, so 10 VUs × 30 iterations = ~300 total. That lines up with what we saw above.
Adding auth — a more realistic test
import http from 'k6/http'import { check, sleep } from 'k6'const BASE = 'http://localhost:8080'export const options = {vus: 50,duration: '2m',thresholds: {http_req_duration: ['p(95)<300'],http_req_failed: ['rate<0.01'],},}// Runs ONCE per VU at startexport function setup() {const r = http.post(`${BASE}/api/auth/login`,JSON.stringify({ email: 'load-test@example.com', password: 'secret123' }),{ headers: { 'Content-Type': 'application/json' } },)return { token: r.json('data.token') }}// Runs in a loop per VU; receives setup data as argumentexport default function (data) {const res = http.get(`${BASE}/api/me`, {headers: { Authorization: `Bearer ${data.token}` },})check(res, { 'me 200': (r) => r.status === 200 })sleep(1)}
setup() runs once before VUs start; its return value is passed to every iteration as data. Use it for: log in once, share the token; create a fixture user, share the ID.
Quick check
Try it
Smoke-test your real API:
- Save the smoke.js script from this lesson.
- Boot your local API:
cd apps/api && go run cmd/server/main.go. - Run k6:
k6 run tests/k6/smoke.js. - Capture the p(95) for http_req_duration. Paste in
notes.md. - Now write a SECOND script for
/api/users(requires auth). Use thesetuppattern from this lesson. - Run it. Compare p(95) to the smoke test.
What's next
Chapter 2 — The five test types. We'll build a full suite: load, stress, spike, soak, with appropriate thresholds for each.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub