Testland
Browse all skills & agents

k6-load-testing

Authors k6 JavaScript load-test scripts (VU loops + checks + sleeps), configures the `options` block with `stages` (ramp-up patterns) and `thresholds` (p(95) latency, error rate), runs via `k6 run script.js` or `--vus / --duration` ad-hoc flags, and uses thresholds as the CI pass/fail signal. Use when the project ships HTTP / WebSocket / gRPC load tests and the team wants developer-friendly JavaScript authoring.

Install with skills.sh (any agent)

npx skills add testland/qa --skill k6-load-testing
View source

k6-load-testing

k6 tests are .js files with a default-exported function that runs once per virtual user (VU) per iteration (per k6-running (opens in new window)). This skill covers the load-testing workflow; k6's browser, synthetic monitoring, and chaos modes share the same script structure but are out of scope here.

When to use

  • The project ships HTTP, WebSocket, or gRPC services that need performance testing.
  • The team prefers JavaScript authoring (vs. JMeter's XML, Gatling's Scala/Java, or Locust's Python).
  • A CI gate is needed on p(95) latency + error rate - k6's thresholds are designed for this.
  • The team is on Grafana Cloud (k6 integrates natively) or wants a self-hosted CLI-only flow.

If the team is already deep in JMeter, the migration cost is non- trivial - evaluate jmeter-load-testing in place. For Python / Locust shops, see locust-load-testing. For JVM / Gatling, see gatling-load-testing.

Install

Install via k6 installation (opens in new window) (brew, apt/yum, choco, Docker). Pin a specific k6 version in CI rather than "latest".

Authoring

Minimal script

import http from 'k6/http';
import { check, sleep } from 'k6';

export default function () {
  const res = http.get('https://quickpizza.grafana.com/');
  check(res, { 'status was 200': (r) => r.status == 200 });
  sleep(1);
}

(Per k6-running (opens in new window).)

The default-exported function runs once per virtual-user iteration. check() is a non-failing assertion that contributes to the checks metric; sleep() simulates think-time between requests.

Options block - stages

Per k6-running (opens in new window), the options export controls the run:

export const options = {
  stages: [
    { duration: '30s',    target: 20 },   // ramp up to 20 VUs over 30s
    { duration: '1m30s',  target: 10 },   // hold ~10 VUs for 90s (gradual scale-down)
    { duration: '20s',    target: 0 },    // ramp down to 0
  ],
};

Three canonical shape patterns:

PatternStages
Smoke testOne stage, { duration: '1m', target: 1 } - sanity check.
Average load testRamp up → plateau → ramp down (the example above).
Stress testRamp past expected peak; observe where the system breaks.
Spike testSudden ramp to high VU; back to normal; verify recovery.
Soak testLong plateau (hours); verify no resource leaks over time.

Options block - thresholds

Per k6-thresholds (opens in new window), thresholds are "the pass/fail criteria that you define for your test metrics" - the canonical CI gate:

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<500'],   // 95% of requests under 500ms
    http_req_failed:   ['rate<0.01'],   // <1% errors
    checks:            ['rate>0.99'],   // >99% of checks pass
  },
};

(Per k6-thresholds (opens in new window).)

The threshold expression syntax is <aggregation_method> <operator> <value>. Available aggregation methods per metric type:

Metric typeMethods
Trendavg, min, max, med, p(N) (percentile, ms)
Countercount, rate
Raterate
Gaugevalue

A test that fails any threshold exits non-zero - the canonical CI gate signal.

abortOnFail

For long-running stress / soak tests, abort early if a threshold is already violated (k6-thresholds (opens in new window)):

thresholds: {
  http_req_duration: [
    { threshold: 'p(95)<500', abortOnFail: true, delayAbortEval: '10s' },
  ],
},

delayAbortEval postpones the abort decision to let metrics accumulate before evaluating; without it, a 1-second test could abort on a single slow request.

Running

Ad-hoc (no options block needed)

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

(Per k6-running (opens in new window).)

--vus overrides the script's options; --duration runs for a fixed time without ramps.

Standard run

k6 run script.js

options block in the script controls everything; the CLI is hands-off. Preferred for CI.

Useful flags

FlagPurpose
--out json=<file>Stream raw metrics to a JSON file.
--summary-export=<file>Write the end-of-test summary to JSON.
--quietSilence the live progress UI (CI noise).
--http-debug=fullVerbose HTTP debug for triage runs.
--env KEY=VALInject env vars accessible via __ENV.KEY.
--config <file>Externalize the options block to a JSON file.

Parsing results

The end-of-run summary prints to stdout; --summary-export writes the same data as JSON for downstream consumption:

{
  "metrics": {
    "http_req_duration": {
      "values": { "p(95)": 432.1, "avg": 198.3, ... },
      "thresholds": { "p(95)<500": { "ok": true } }
    },
    "http_req_failed": {
      "values": { "rate": 0.004, "passes": 996, "fails": 4 },
      "thresholds": { "rate<0.01": { "ok": true } }
    }
  }
}

Pipe to jq for a quick gate report:

jq -r '
  .metrics
  | to_entries[]
  | select(.value.thresholds)
  | .key + ": " + (.value.thresholds | to_entries | map("\(.key) → \(if .value.ok then "PASS" else "FAIL" end)") | join(", "))
' summary.json

CI integration

Full GitHub Actions workflow - apt install, PR + nightly triggers, summary upload via if: always() - in references/ci-integration.md. A failing threshold causes k6 run to exit non-zero, failing the job.

Anti-patterns

Anti-patternWhy it failsFix
Hard-coded URLs / tokens in scriptScripts bind to one environment; secrets leak.Read from __ENV.API_BASE_URL, __ENV.API_TOKEN.
--vus 1000 --duration 1h from a developer laptopLaptop resource contention; client-side bottlenecks corrupt metrics.Run from a CI runner / dedicated load generator; never from a dev box for serious numbers.
Threshold p(95)<10000Practically meaningless - passes any sane API. Hides regressions.Set thresholds at meaningful budgets (500ms, 1s) tied to NFRs from the non-functional-requirement-extractor.
Soak tests in PR CIMulti-hour PR CI; team disables.Soak tests are scheduled-only; PRs run smoke / average load.
Missing sleep() between requestsHammering at full VU rate generates synthetic numbers; doesn't model real users.Include sleep(1) or randomized think-time after each iteration.
Asserting only http_req_failed rateA 30-second response that succeeds passes the rate gate but breaks UX.Always pair with http_req_duration percentile thresholds.

Limitations

  • Single-machine VU limits. A single k6 instance saturates one machine's outbound CPU / network - typically 20-50k concurrent VUs for HTTP. For higher loads, distribute via Grafana Cloud or k6 Operator on Kubernetes.
  • No native browser execution. The k6/browser module covers it but adds Chromium overhead; for browser-driven perf testing, consider lighthouse-perf instead.
  • JS sandbox limits. k6 uses goja (Go-based ES5+/some-ES6), not Node.js. npm packages with native deps don't work; pure-JS packages do. Use k6 archive to bundle.

References

k6 CI integration

A GitHub Actions workflow that installs k6 from the official apt repository, runs the load test on pull requests that touch tests/load/** and on a nightly schedule, and uploads the JSON summary. A failing threshold causes k6 run to exit non-zero and fail the job; the summary artifact is uploaded regardless via if: always().

# .github/workflows/load-test.yml
name: load-test

on:
  pull_request:
    paths:
      - 'tests/load/**'
  schedule:
    - cron: '0 4 * * *'   # nightly soak

jobs:
  k6:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Install k6
        run: |
          sudo gpg -k
          sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
            --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
          echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
          sudo apt-get update
          sudo apt-get install k6

      - name: Run k6 test
        env:
          API_BASE_URL: ${{ secrets.STAGING_BASE_URL }}
          API_TOKEN:    ${{ secrets.STAGING_API_TOKEN }}
        run: |
          k6 run \
            --summary-export=summary.json \
            --quiet \
            tests/load/orders.js

      - name: Upload summary
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: k6-summary
          path: summary.json
          retention-days: 14

Pin a specific k6 version in CI for determinism rather than tracking stable.

Related skills

db-query-plan-analyzer

Reads `EXPLAIN` / `EXPLAIN ANALYZE` output from PostgreSQL, MySQL, or SQLite - identifies the dominant cost (sequential scan, nested loop, sort spill, missing index, type-cast preventing index use), proposes the specific index or query rewrite to fix it, and emits the candidate `CREATE INDEX` statement. Use when load testing or production telemetry shows the database as the bottleneck and the team needs targeted query-level remediation.

flame-graph-analyzer

Reads CPU flame-graph output from py-spy (Python), async-profiler (JVM), Go pprof, or Node.js `perf_hooks` / clinic.js: identifies the hot path (top sample-time frames), classifies the bottleneck (CPU-bound vs lock contention vs allocator pressure), and proposes the next investigation step. Use when a perf regression is bisected to a commit but the hot path inside it is unclear; for tail-latency percentiles use latency-percentile-analyzer, for GC pauses specifically use jvm-gc-tuning, and for a slow SQL hot path use db-query-plan-analyzer.

gatling-load-testing

Authors Gatling simulations in Java / Kotlin / Scala (or JS / TS) using the Simulation class plus http() / scenario() / exec() DSL builders, ramps virtual users via injectOpen (arrival rate) or injectClosed (concurrent count), runs via Maven / Gradle / sbt with the Gatling plugin, and gates CI on assertions defined in setUp(). Use when the project is on the JVM and the team prefers code-first load tests over JMeter's XML or k6's JavaScript-only authoring.

jmeter-load-testing

Authors Apache JMeter `.jmx` test plans (Thread Groups + HTTP samplers + assertions + listeners) in the JMeter GUI, runs them headlessly via `jmeter -n -t plan.jmx -l results.jtl`, generates an HTML dashboard with `-e -o`, and gates CI on JTL parsing. Use when the project has an existing JMeter investment, needs JVM-native load tooling, or works in domains with strong JMeter community support (banking, telecom, enterprise).

jvm-gc-tuning

Diagnoses JVM garbage-collection behaviour under load: reads and interprets unified GC logs (-Xlog:gc*), selects the right collector (G1 vs ZGC vs Parallel vs Serial), tunes heap sizing and pause-time targets, quantifies allocation rate, and traces the GC-pause-to-latency-tail link using GCViewer and Java Flight Recorder (JFR). Use when a load test reveals p99/p999 latency spikes that correlate with GC activity, or when heap sizing and collector selection need justification before a performance baseline is locked.

latency-percentile-analyzer

Interprets latency distributions from k6 load tests beyond the p95/p99 gate: reads percentile summaries and JSON exports to identify tail shape, computes the tail ratio (p99/p50) as a distribution-spread signal, detects bimodal distributions, explains coordinated omission and why naive p99 values are optimistic under sustained load, and distinguishes request-rate from concurrency models. Use when a k6 threshold passes but the system still feels slow, when p99 is suspiciously low during ramp-up, or when the team needs to explain why tail latency is high rather than just observing that it is.

lighthouse-budget-author

Drafts a `lighthouserc.js` (or `budget.json`) at design time - picks Web Vitals thresholds (LCP / INP / CLS) per route based on traffic class (cached / dynamic / API-heavy / form-heavy) and the team's NFRs, plus resource-size budgets (JS / CSS / images / total bytes). Emits the config file ready for the lighthouse-perf runner. Use when starting Lighthouse coverage on a project that has no budgets yet, or when the existing budgets need a redesign.

lighthouse-perf

Configures Lighthouse CI (`@lhci/cli`) to audit Web Vitals (LCP, INP, CLS) on every PR, asserts against canonical thresholds (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1 at the 75th percentile), uploads Lighthouse reports as build artifacts, and posts deltas as PR comments. Use when the project ships a web frontend and the team needs continuous Web Vitals monitoring tied to PR gating.

load-testing-overview

Teaches load and performance testing from zero: how to choose between k6, JMeter, Gatling, Locust, and Artillery based on observable project facts (team language, tests-as-code vs GUI authoring, protocols beyond HTTP, CI gating needs); the six load profiles (smoke, average-load, stress, spike, soak, breakpoint) and the question each one answers; the difference between open workload models that hold arrival rate constant and closed models that hold concurrent users constant; why percentiles rather than averages are the unit of measurement; and how to turn a run into a pass/fail CI gate, with a first runnable k6 script. Use when a service needs performance coverage and the tool, the load profile, or the pass/fail threshold has not been decided yet.

locust-load-testing

Authors Locust load tests as Python classes - HttpUser with @task-decorated methods plus on_start hooks and between() wait_time - runs via `locust -f locustfile.py` headless mode (or distributed via `--master` / `--worker`), and exports CSV / JUnit reports for CI gating. Use when the project's primary stack is Python and the team wants load tests in the same language as the application.

perf-budget-gate

Builds a unified release-readiness gate that aggregates verdicts from any combination of k6 / JMeter / Gatling / Locust load runners and Lighthouse CI Web Vitals, applies severity-aware pass/fail thresholds, and emits a single go / no-go decision with per-metric deltas vs the main-branch baseline. Posts the delta as a PR comment when the team has the integration set up. Use when authoring a CI step that gates a deployment on cross-runner perf compatibility.

slo-load-test-plan

Turns a service's SLOs and endpoint traffic mix into a named scenario matrix: one scenario per SLO boundary condition, a load profile (smoke, average-load, stress, soak, spike, breakpoint) per scenario, an open or closed workload injection model, a threshold expression derived from the SLO the scenario guards, and an error-budget calculation that sets the soak run's failure allowance. Stays runner-agnostic and fixes the pass/fail line before any tool is configured. Use when an SLO document and an endpoint list both exist but nobody has decided which load runs to make, what shape of load each carries, or what number would count as a failure.