Performance Testing
Intermediate 13 min read Performance Testing

k6 Scripting

k6 is a scriptable load testing tool where test logic is plain JavaScript and load profiles are declared in an options block. Learn the default function, checks, thresholds, executors and the CI-friendly exit code.

k6default functionchecksthresholdsvusexit code
Prerequisites: Metrics & Baselines
Introduction

What is this?

k6 is a developer-focused, scriptable load testing tool built by Grafana Labs. Test logic is written in JavaScript: a default function that every virtual user executes, plus an options block that declares the load profile. It is Go-based under the hood, so a single instance can sustain tens of thousands of virtual users, and it ships with checks, thresholds and native JSON, InfluxDB and Prometheus output.

Motivation

Why do you need it?

  • Scripts are code: git-reviewable, reusable across teams and testable with plain assertions.
  • Declarative thresholds fail a run when budgets are breached, giving you a built-in CI gate.
  • Native HTTP/1.1, HTTP/2, WebSocket and gRPC support, and a single process scales to roughly 40k+ VUs.
  • The CLI, REST API and .js scripts integrate with existing build systems without special daemons.
  • Rich built-in metrics: http_req_duration, http_req_failed, vus, iterations and custom metrics.
Think about it

Mental model

Imagine

Test-driving a car on a course

Think of

k6 scripting

The default function is the lap the driver repeats; options are the course conditions (how many drivers, how fast they enter); checks are the flags waved on each lap; thresholds are the rules that decide whether the lap time qualifies.

Mechanics

How it works

  • The default function is executed once per iteration by each virtual user — it is the workload your test applies.
  • The options block configures load: executor (ramping-vus, constant-arrival-rate, shared-iterations), stages and thresholds.
  • Checks validate per-response conditions and count passes and failures in the summary.
  • Thresholds evaluate metric expressions after the run and set the process exit code (0 = pass, 1 = fail).
  • Results stream to stdout, JSON, CSV, InfluxDB or Prometheus so you can build dashboards.

How a k6 run executes iterations and enforces thresholds.

Load script
Executors schedule VUs
Each VU runs default()
Metrics stream
Thresholds evaluate
Exit code
Reference

Key commands

k6 run script.js

Run the script with the load profile declared in its options block.

default     ✓✓ 120s 2000 iterations · 0 failed
k6 run --vus 100 --duration 5m script.js

Override the load profile from the CLI without editing the script.

k6 run --iterations 1000 script.js

Stop after a fixed number of iterations instead of a time window.

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

Stream every metric point to JSON for post-processing and storage.

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

Push results directly to InfluxDB for dashboarding in Grafana.

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

Control which percentiles the summary prints for trend metrics.

Hands-on

Examples

A k6 load test with checks and thresholds

A k6 load test with checks and thresholds
import http from "k6/http"
import { check, sleep } from "k6"
export const options = {
vus: 50,
duration: "2m",
thresholds: {
http_req_failed: ["rate<0.01"],
http_req_duration: ["p(95)<250", "p(99)<500"],
},
}
export default function () {
const res = http.get("https://api.example.com/v1/items?page=1", {
headers: { Authorization: "Bearer token" },
tags: { endpoint: "list-items" },
})
check(res, {
"status is 200": (r) => r.status === 200,
"body is JSON": (r) => r.json() !== null,
})
sleep(1)
}

Fifty virtual users hit the endpoint for two minutes. Every iteration checks the status code and that the body parses as JSON, then pauses one second. Thresholds encode the budget: the run fails if errors exceed 1% or p95 exceeds 250ms, and the non-zero exit code is what CI consumes.

Real world

In production

  • k6 runs as a Kubernetes CronJob or GitHub Actions step, gating merges with the exit-code-based thresholds.
  • Scripts share test logic through the JS module system so suites stay DRY across teams.
  • Results stream to Prometheus or InfluxDB, and the same dashboards show perf tests and production trends side by side.
  • Distributed load across regions coordinates k6 instances (or k6 Cloud) without managing agent fleets.
Guidelines

Best practices

  • Keep the default function small and realistic — include think time (sleep) and varied payloads.
  • Tag requests (tags: { endpoint, region }) so trend metrics can be sliced per endpoint in dashboards.
  • Use thresholds as the single source of truth for pass/fail instead of eyeballing summary output.
  • Pin the k6 version in CI so script semantics don't drift between runs.
  • Store scripts in git with the same review bar as application code.
Watch out

Common mistakes

  • Forgetting sleep() and running a tight loop, which drives unrealistic server pressure.
  • Relying on the default function alone and never using executors, so the load profile isn't expressible.
  • Ignoring http_req_failed in thresholds — latency numbers are meaningless when errors are climbing.
  • Running k6 from a laptop on Wi-Fi and blaming the app for client-side jitter.
Root cause analysis

Common issues

Error
TypeError: Cannot read properties of undefined (reading 'status')
Root cause

The request threw before returning a response object, or the URL is unreachable.

Fix

Guard with if (res) before accessing fields, and confirm the URL with curl first.

Error
threshold 'http_req_failed' has an invalid expression
Root cause

A typo in the threshold expression.

Fix

Use valid expressions like rate<0.01 and p(95)<250, comma-separated inside the array.

Error
dial tcp: lookup api.example.com: no such host
Root cause

DNS failure or a wrong hostname.

Fix

Verify the hostname resolves with nslookup and is reachable with curl.

Error
insufficient VUs, reached the max VU limit
Root cause

The load profile requires more virtual users than k6 allows on the process.

Fix

Raise the limit with K6_VUS_MAX or scale out to multiple k6 instances / distributed load.

When things break

Debugging tips

  • Run a single VU with --vus 1 -i 1 and log res.body to verify the script before scaling load.
  • Use http_req_connecting and http_req_tls_handshaking trends to isolate network and TLS overhead from app latency.
  • Slice thresholds per tag, e.g. http_req_duration{endpoint:list-items}, to isolate a slow path.
  • Compare runs with and without connection reuse to measure its cost explicitly.
Interviews

Interview checkpoint

Practice

Hands-on lab

Build a CI-Ready k6 Test with Thresholds
35 min · intermediate
In progress
Objectives
  • Write a k6 script with checks and realistic think time
  • Tag requests by endpoint for sliceable metrics
  • Declare thresholds for latency and error rate
  • Run in headless mode and read the exit code as a CI gate

Build a CI-Ready k6 Test with Thresholds

35 min · intermediate

0/5 steps
0%
sandbox · Linux sandbox
$
Try these commands
Test yourself

Quick quiz

0/3
1

Which part of a k6 script runs for every virtual user iteration?

2

A k6 run exits with a non-zero code if any threshold expression is violated.

3

You want a test that stops after exactly 10,000 requests, regardless of time. Which approach fits best?

Quick reference

Cheat sheet

k6 essentials
k6 run script.jsExecute with options from the script
--vus / --durationOverride load from the CLI
--iterations NStop after N iterations
--out json=out.jsonStream results to JSON
--summary-trend-stats 'p(95)'Choose printed percentiles
thresholdsDeclare pass/fail budgets
Go deeper

Official documentation