Running K6 and Reading Output

8 min read

The K6 terminal output can look overwhelming the first time. There are twenty-odd metric rows, several statistical columns, and unit abbreviations that are not immediately obvious. This lesson decodes every section, explains which numbers to look at first, and shows you how to customise what K6 reports.

The basic run commands

Run with defaults — one VU, one iteration:

k6 run script.js

Override VUs and duration from the CLI without editing the script:

k6 run --vus 10 --duration 30s script.js

This runs 10 virtual users for 30 seconds. Each VU continuously loops the export default function until 30 seconds elapse. K6 then waits for any in-flight requests to complete and prints the summary.

The full output, annotated

Here is the output from a 10-VU, 30-second run against a simple HTTPS endpoint:

  execution: local
     script: script.js
     output: -
 
  scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration
           * default: 10 looping VUs for 30s (gracefulStop: 30s)
 
✓ status is 200
 
checks.........................: 100.00% ✓ 286   ✗ 0
data_received..................: 3.4 MB  112 kB/s
data_sent......................: 25 kB   832 B/s
http_req_blocked...............: avg=2.41ms min=1µs    med=3µs    max=211ms  p(90)=5µs    p(95)=8µs
http_req_connecting............: avg=576µs  min=0s     med=0s     max=65ms   p(90)=0s     p(95)=0s
http_req_duration..............: avg=134ms  min=101ms  med=127ms  max=612ms  p(90)=173ms  p(95)=201ms
  { expected_response:true }..: avg=134ms  min=101ms  med=127ms  max=612ms  p(90)=173ms  p(95)=201ms
http_req_failed................: 0.00%   ✓ 0     ✗ 286
http_req_receiving.............: avg=412µs  min=56µs   med=224µs  max=18ms   p(90)=860µs  p(95)=1.28ms
http_req_sending...............: avg=48µs   min=10µs   med=38µs   max=2.2ms  p(90)=80µs   p(95)=103µs
http_req_tls_handshaking.......: avg=1.44ms min=0s     med=0s     max=183ms  p(90)=0s     p(95)=0s
http_req_waiting...............: avg=133ms  min=100ms  med=127ms  max=611ms  p(90)=172ms  p(95)=200ms
http_reqs......................: 286     9.47/s
iteration_duration.............: avg=1.14s  min=1.10s  med=1.13s  max=1.62s  p(90)=1.18s  p(95)=1.22s
iterations.....................: 286     9.47/s
vus............................: 10      min=10  max=10
vus_max........................: 10      min=10  max=10

The five metrics that matter most

Understanding percentiles

Every timing metric reports avg, min, med, max, p(90), and p(95). The percentiles are the numbers to care about.

p(95)=201ms means 95% of requests completed in 201ms or less — the slowest 5% took longer. This is a far more reliable SLA target than the average, because averages are pulled down by fast requests and hide the tail.

In the example output above:

  • avg=134ms — tells you the middle of the distribution, masking outliers
  • p(95)=201ms — the number to write into your SLA and your K6 threshold
  • max=612ms — one request, likely a cold TCP connection or a database stall

Always use p(95) or p(99) for performance thresholds, not the average. This is one of the most common mistakes in performance testing across all tools.

The http_req_duration breakdown

K6 splits each HTTP request into sub-timings that show exactly where time was spent:

MetricMeasures
http_req_blockedTime waiting for a free TCP connection from the pool
http_req_connectingTCP handshake time
http_req_tls_handshakingTLS setup (0 for HTTP, non-zero for HTTPS)
http_req_sendingTime to upload the request body
http_req_waitingTime from sending request to receiving first byte (server processing)
http_req_receivingTime to download the response body
http_req_durationTotal: sending + waiting + receiving

If http_req_waiting is high, the server is slow to process. If http_req_receiving is high, the response body is large. If http_req_blocked is consistently high, your connection pool is exhausted and you are queuing requests — a common sign that the server is saturated.

In JMeter, you get "elapsed time" — equivalent to http_req_duration. K6's breakdown is built in and requires no extra configuration, which is one of its significant diagnostic advantages.

Customising K6's output

Show additional percentiles in the summary:

k6 run --summary-trend-stats="avg,p(50),p(90),p(95),p(99),max" script.js

Export the full end-of-test summary to JSON for scripted analysis or CI comparison:

k6 run --summary-export=results.json script.js

Stream every data point to InfluxDB for live Grafana dashboards:

k6 run --out influxdb=http://localhost:8086/k6 script.js

Write a JSON line per metric sample during the test (useful for custom post-run analysis):

k6 run --out json=results.json script.js

Multiple outputs can be combined in one run:

k6 run --out json=results.json --out influxdb=http://localhost:8086/k6 script.js

Reading the scenario block

The header block before metrics tells you what K6 actually ran:

scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration
         * default: 10 looping VUs for 30s (gracefulStop: 30s)

gracefulStop: 30s is K6's built-in buffer time after the test duration expires. VUs stop accepting new iterations but are given 30 seconds to complete any iteration that was already in progress. This prevents dropped requests at the test boundary.

⚠️ Common mistakes

  • Using the average as your SLA target. Averages are pulled down by fast requests and hide the slow tail. The 5% of users who experience the worst latency are the ones who churn. Always define SLAs on p(95) or p(99).
  • Ignoring http_req_failed. A 1% failure rate on 10,000 requests is 100 errors. Always read this metric even when your p(95) looks fine — a low failure rate hiding 4xx responses means your test data or auth setup has a problem.
  • Confusing iterations with http_reqs. One iteration of the default function may contain multiple HTTP requests. If your script calls three endpoints per iteration, http_reqs will be approximately 3× iterations. Read both and understand what your test actually does.

🎯 Practice task

Run the same script twice and compare the output. 20 minutes.

  1. Use a script with vus: 10, duration: '30s' and sleep(1). Run it and note http_req_duration p(95) and http_reqs rate.
  2. Change sleep(1) to sleep(0.5) and run again. How did iterations, request rate, and http_req_duration change?
  3. Add --summary-trend-stats="avg,p(50),p(90),p(95),p(99),max" to the run command. Which percentile shows the most variation between the two runs?
  4. Run with --out json=run.json and open the file. Find one line where metric is http_req_duration. Identify its type, data.value, and data.tags fields.

This practice makes K6 output readable by feel — the skill you need before interpreting results from complex multi-stage test runs.

// tip to track lessons you complete and pick up where you left off across devices.