Debug
Table of Contents
- Why one iteration first
- How: run exactly one iteration
- How:
--http-debug - How: print a response body
- How:
k6 inspect - How: common errors
- Tips
Why one iteration first
A script with a typo in a JSON path or a wrong header fails the same way at 1 VU as it does at 50 — except at 50 VUs the terminal fills with the same error 50 times a second and the actual HTTP request/response that caused it scrolls off screen. Every debugging session in this track starts by cutting the noise down to one VU, one iteration, before looking closer.
How: run exactly one iteration
k6 run -u 1 -i 1 03-order-flow.js
-u 1 (or --vus 1) pins the VU count, -i 1 (or --iterations 1) caps the total iterations at one, overriding whatever options.vus/iterations the script sets. Real output from a run against the playground:
running (00m00.0s), 1/1 VUs, 0 complete and 0 interrupted iterations
default [ 0% ] 1 VUs 00m00.8s/10m0s 0/1 shared iters
█ THRESHOLDS
checks
✓ 'rate==1' rate=100.00%
█ TOTAL RESULTS
checks_total.......: 3 2.274099/s
checks_succeeded...: 100.00% 3 out of 3
checks_failed......: 0.00% 0 out of 3
✓ menu 200
✓ order 201
✓ order number assigned
...
running (00m01.3s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [ 100% ] 1 VUs 00m01.1s/10m0s 1/1 shared iters
One pass through setup() and the default function, three checks, done in just over a second — small enough to read every line.
How: --http-debug
--http-debug logs every request and response line k6 makes, without bodies. Real output (trimmed to one request/response pair) from k6 run -u 1 -i 1 --http-debug 03-order-flow.js:
time="..." level=info msg="Request:\nPOST /api/v1/orders HTTP/1.1\nHost: 127.0.0.1:8080\nUser-Agent: Grafana k6/2.2.0\nContent-Length: 202\nAuthorization: Bearer eyJhbGciOiJIUzI1NiJ9...\nContent-Type: application/json\nAccept-Encoding: gzip\n\n\n" group= iter=0 name="POST /orders" request_id=4892d3e4-577d-4e91-9aa5-be5eb35cbc00 scenario=default source=http-debug vu=1
time="..." level=info msg="Response:\nHTTP/1.1 201 \nTransfer-Encoding: chunked\nCache-Control: no-cache, no-store, max-age=0, must-revalidate\nContent-Type: application/json\nDate: Mon, 28 Sep 2026 05:45:06 GMT\n...\n\n\n" group= iter=0 name="POST /orders" request_id=4892d3e4-577d-4e91-9aa5-be5eb35cbc00 scenario=default source=http-debug vu=1
Each request/response pair on v2.2 is one structured log line with msg holding the raw HTTP head (\n-escaped, not actually rendered), and fields (group, iter, name, request_id, scenario, vu, source=http-debug) that tell you exactly which VU/iteration/named request it belongs to — the name tag from Checks and Thresholds doubles as a debug filter here. Bodies are empty (\n\n\n at the end) because plain --http-debug only logs headers.
--http-debug=full adds the bodies. Real output, same request, with =full:
time="..." level=info msg="Request:\nPOST /api/v1/orders HTTP/1.1\n...\nAccept-Encoding: gzip\n\n{\"type\":\"PICKUP\",\"customer\":{\"name\":\"k6 load test\",\"email\":\"customer01@playground.local\",\"phone\":\"0100000000\"},\"items\":[{\"pizzaId\":4,\"sizeId\":1,\"crustId\":2,\"toppingIds\":[],\"quantity\":1}],\"autoPay\":true}\n" group= iter=0 name="POST /orders" request_id=2e435487-... scenario=default source=http-debug vu=1
time="..." level=info msg="Response:\nHTTP/1.1 201 \n...\n\n53\n{\"orderId\":\"5a4a2f41-a72d-4b1d-b7c5-50f7c789cab5\",\"orderNumber\":1001,\"payUrl\":null}\n0\n\n\n" group= iter=0 name="POST /orders" request_id=2e435487-... scenario=default source=http-debug vu=1
That's the exact request body createOrder built (with the correlated pizzaId/sizeId/crustId from the menu response) and the exact response it got back — this is what confirms payUrl is null when autoPay: true, and what a chunked response looks like on the wire (53\n{...}\n0\n\n, the chunk-size header k6 doesn't strip when logging raw). --http-debug=full is noisy fast — reach for it after -u 1 -i 1 has already narrowed things down to one request worth looking at, not as the first thing to turn on.
How: print a response body
Sometimes the header/body dump from --http-debug is more than needed and only one field or the whole parsed body is in question. console.log(JSON.stringify(res.json(), null, 2)) prints it formatted. Real output for the /menu response (trimmed):
{
"toppings": [
{ "name": "Extra Cheese", "priceDelta": 2.5, "id": 1 },
...
],
"pizzas": [
{ "id": 1, "name": "Margherita", "description": "Tomato, mozzarella, basil", "basePrice": 18, "category": "classic", "imageUrl": "/placeholder.svg" },
...
],
"sizes": [
{ "id": 1, "name": "Regular", "priceDelta": 0 },
...
],
"crusts": [
{ "id": 1, "name": "Classic", "priceDelta": 0 },
...
]
}
Note the field order varies between array entries (id/name/priceDelta isn't in the same order every time) — that's the server's JSON serialization, not a script bug, and a reminder not to assert on key order or array position, only on values (pick(menu.pizzas).id, never menu.pizzas[0], would be a fragile assumption).
How: k6 inspect
k6 inspect script.js prints the script's resolved options as JSON, without running it — useful for confirming what a script will actually do (thresholds, scenarios, stages) before spending the time on a real run. Real output for 04-thresholds.js (trimmed):
{
"paused": null,
"vus": 3,
"duration": "30s",
...
"thresholds": {
"checks": [
"rate==1"
],
"group_duration{group:::order}": [
"avg<3000"
],
"http_req_duration": [
{
"threshold": "p(99)<3000",
"abortOnFail": true,
"delayAbortEval": "10s"
}
],
"http_req_duration{name:GET /menu}": [
"p(95)<300"
],
"http_req_duration{name:POST /orders}": [
"p(95)<1000"
],
"http_req_failed": [
"rate<0.005"
],
"order_to_paid": [
"p(95)<5000"
]
},
...
}
Every threshold from the script is there, verbatim — < shows up JSON-escaped as <, which is normal encoding/json behaviour on Go's side, not a sign anything is wrong.
How: common errors
Three errors this track's scripts can hit, each reproduced for real — recognize the message, then apply the fix next to it:
Status 0 — no response. Pointing 03-order-flow.js at a port nothing is listening on:
time="..." level=error msg="Error: login owner@playground.local: no response from http://127.0.0.1:9/api/v1. Is the playground running? (docker compose up -d)\n\tat expectOk (file://.../lib/auth.js:6:31(14))\n\tat login (file://.../lib/auth.js:16:18(33))\n\tat resetPlayground (file://.../lib/auth.js:25:22(5))\n\tat setup (file://.../03-order-flow.js:16:18(3))\n" hint="script exception" source=stacktrace
res.status === 0 is k6's signal that the request never got a response at all (connection refused, DNS failure, timeout) — not any HTTP status the server sent. lib/auth.js's expectOk checks for it explicitly and throws a message that says so, rather than letting it fall through to the generic >= 400 branch and reporting a misleading "HTTP 0" error.
open() outside init code. Calling open() from inside the default function instead of at module scope:
time="..." level=error msg="GoError: the \"open\" function is only available in the init stage (i.e. the global scope), see https://grafana.com/docs/k6/latest/using-k6/test-lifecycle/ for more information\n\tat go.k6.io/k6/v2/internal/js.(*Bundle).setInitGlobals.func3 (native)\n\tat default (file://.../open-in-func.js:2:7(3))\n" executor=per-vu-iterations hint="script exception" scenario=default source=stacktrace
This is the constraint behind lib/data.js building its SharedArray at module scope, never inside the default function.
Mutating a SharedArray. Calling .push() on one after creation:
time="..." level=error msg="TypeError: SharedArray is immutable\n\tat push (native)\n\tat default (file://.../shared-mutate.js:4:11(4))\n" executor=shared-iterations hint="script exception" scenario=default source=stacktrace
SharedArray is deliberately frozen for the reason covered in Test data — one shared block of memory across every VU means no VU can be allowed to mutate it out from under the others.
Tips
- Read the error's stack trace, not just its first line. k6's
GoError/TypeErrormessages point at the exactlib/*.jsline, same as a Node stack trace —expectOkabove shows the whole chain fromsetup()down to the actual failing request. -u 1 -i 1first,--http-debugsecond,console.logthird. Cutting to one iteration usually shows which check or which request failed;--http-debug(then=fullif headers alone aren't enough) shows what actually went over the wire;console.log(JSON.stringify(...))is for when the response parsed fine but a specific field's value or shape is in question.