Generated banner for K6 Perf Test Website
Generated logo for K6 Perf Test Website

K6 Perf Test Website

Grafana LabsOfficialPublished by Grafana Labs

  • Skill
  • Workflow
  • Cloud and DevOps
  • Apache-2.0
  • Low risk
  • Has scripts

From the author

Use when the user wants to performance-test, load-test, or stress-test a public website end-to-end with k6. Produces a hybrid (protocol + browser) test suite, SLO-backed thresholds, a load-generator monitor sidecar, and a Grafana-side investigation playbook for backends the user owns. Triggers on "perf test my site", "performance test my site", "load test this URL", "stress test my web app", "I want to load test [URL]", "set up k6 against my website", "write a k6 suite for [site]", "see if my site handles N concurrent users", or "how does my site perform under traffic". Use this skill whenever the user mentions k6, load testing, stress testing, performance testing, or wants to validate a website under traffic — even if they don't explicitly use the word "test" or ask for the specific outputs this skill produces. This skill is for a full multi-test suite across several workflows; for a single browser test — functional OR a browser load test measuring Web Vitals for one journey — use k6-browser-test.
— the description by Grafana Labs

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides an opinionated, structured workflow for performance, load, and stress testing public websites. It generates a complete test suite including functional protocol and browser tests, hybrid load scenarios, SLO-backed thresholds, and monitoring sidecars. Designed for teams that need to validate website performance under traffic, it emphasizes a rigorous process: eliciting workflows, ensuring functional tests pass, and then executing various load test types (smoke, average, stress, spike, soak, breakpoint) with proper monitoring and reporting.

What it does

  • Scaffolds a complete performance testing project structure.
  • Facilitates recording user workflows using Playwright.
  • Builds functional protocol and browser tests to ensure baseline stability.
  • Configures hybrid load tests with SLO-backed thresholds and per-endpoint tagging.
  • Provides a load-generator monitor sidecar to ensure test validity.
  • Generates structured reports and investigation playbooks for backend analysis.

When to use it

  • When you need to perform load, stress, or performance testing on a public website.
  • When you want to validate how a website handles specific concurrent user loads.
  • When you need a full suite of tests rather than a single functional browser test.
  • When you need to investigate backend performance bottlenecks under simulated traffic.

Advantages

  • Enforces a disciplined, repeatable testing methodology.
  • Reduces guesswork by requiring explicit workflow elicitation.
  • Ensures load tests are only run against functional, stable code.
  • Provides visibility into both protocol-level performance and real-world browser experience.
  • Includes built-in monitoring to prevent false results caused by load-generator bottlenecks.

Usage examples

“perf test my site”
The agent will initiate the workflow elicitation process to understand your site's critical paths and testing requirements.
“see if my site handles 500 concurrent users”
The agent will guide you through setting up a stress test scenario with appropriate thresholds to evaluate site capacity.
“stress test my web app”
The agent will scaffold the project and help you record the necessary workflows to begin building a hybrid load test suite.
“how does my site perform under traffic”
The agent will help you design a series of load tests (smoke, average, spike) to characterize your site's performance profile.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/grafana/k6-perf-test-website/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip k6-perf-test-website.zip -d .agents/skills

Documentation by Grafana Labs

k6-perf-test-website

An end-to-end, opinionated workflow for performance-testing any public website with k6. The skill produces:

  • A scaffolded project with one folder per user-described workflow.

  • Functional protocol + browser tests (must pass before load testing).

  • Hybrid load tests (protocol scenario + 1 browser VU) per test type (smoke / average / stress / spike / soak / breakpoint).

  • SLO-backed thresholds with per-endpoint tagging and Web Vitals.

  • A cross-platform load-generator monitor sidecar.

  • Optional Grafana-side investigation when the user owns the backend.

  • A structured Markdown report at the end.

Just want one functional browser test from a plain-language journey? This skill builds a full, multi-test performance suite. For a single k6/browser functional (or standalone browser load) test — with selectors discovered by exploring the live site — use the k6-browser-test skill instead.

This skill enforces a few opinions you should not silently override:

  • Always elicit workflows first. Don't guess.

  • Functional tests must be green before load tests run.

  • Always monitor the load generator — server-looks-slow is often laptop-looks-slow.

  • Local for validation, cloud for scale — but ask the user where to run each test type; don't hardcode.

  • No shared tests/lib/. Iteration-body duplication is preferred; each script reads cleanly on its own during incident review.

Prerequisites

  • k6 ≥ v2.0.0 (k6 version) — required for stable k6/browser, expect(), async/await iteration functions, and per-request tags.

  • Node.js ≥ 20 + npm.

  • Playwright with the Chromium download (npx playwright install chromium).

  • har-to-k6 (npm i -D har-to-k6).

  • Network access from the load-generator host to the target site.

Tools the skill prefers when installed:

  • mcp-k6 — script creation, Playwright→k6/browser migration, API lookup. Prefer over hand-writing k6 boilerplate.

  • mcp-grafana — in-session Prometheus/Loki/Tempo/Pyroscope queries during §9 backend investigation.

  • gcx — Grafana Cloud CLI for shell-friendly queries, datasource discovery, and Grafana Cloud k6 cloud-run dispatch.

  • k6 binary — local validation runs and breakpoint hunting.

  • k6 x docs (xk6-docs) — look up k6 API surface when writing or editing scripts without mcp-k6 available.

If these tools are not configured the skill falls back to plain CLI tools (k6, npx, curl) and hand-written scripts. The skill does not own toolchain setup; defer to the user's existing setup process.

Explicit non-goals:

  • Protocol-only suites (out of scope for this skill).

  • API-only / non-browser apps.

  • Mobile-native testing.

  • Capacity planning beyond finding and tagging a breakpoint.

Workflow overview

Tick these off in order. Each step has a section below.

  1. Elicit workflows from the user. §1

  2. Scaffold the project from assets/. §2

  3. Record each workflow with Playwright. §3

  4. Build functional protocol + browser tests; run tests/run-all.sh until green. §4

  5. Design SLO-backed thresholds and per-endpoint tags. §5

  6. Build hybrid load tests, one file per test type. §6

  7. Run validation locally with the LG sidecar. §7

  8. Push to Grafana Cloud k6 for the test types the user chose for cloud. §8

  9. Investigate the backend with Grafana (if owned). §9

  10. Report back to the user. §10

1. Elicit workflows

The single most important step. Without explicit workflows, every later step is guesswork.

Ask the user the questions in references/workflow-elicitation.md and record answers in a runbook.md alongside the scaffolded project.

You must capture: 2-4 named workflows, credentials, read vs write, destructive actions to avoid during soak, worry list, existing SLOs, backend ownership and Grafana access, and per test type whether each runs locally or in Grafana Cloud k6.

If the user can't name at least one workflow, stop and clarify; do not proceed.

2. Scaffold the project

Copy the assets/ tree from this skill into the user's chosen directory. The skill's assets/ directory is at <SKILL_DIR>/assets/, where <SKILL_DIR> is the absolute path to this skill's directory — your harness exposes this (e.g. opencode prefixes skill metadata with a Base directory for this skill: line). If you can't determine <SKILL_DIR> from context, ask the user.

cp -R "<SKILL_DIR>/assets/." "<target-dir>/"

If cp -R is blocked by sandbox permissions, copy files individually via your agent's file-write tool.

The scaffolded layout:

<target-dir>/
├── package.json
├── .gitignore
├── README.md
├── runbook.md                       # you create from §1 answers
├── recordings/
│   ├── README.md
│   └── scripts/
│       └── recorder.template.js     # copy per workflow → wN-<short-name>.js, …
├── tests/
│   ├── run-all.sh
│   └── workflow.template/           # copy per workflow → wN-<short-name>/, …
│       ├── from-har.js
│       ├── protocol.js
│       ├── browser.js
│       ├── smoke.js
│       ├── average.js
│       ├── stress.js
│       ├── spike.js
│       ├── soak.js
│       └── breakpoint.js
└── tools/
    ├── lg-monitor.sh
    └── run-with-monitor.sh

For each workflow: copy recorder.template.js → recordings/scripts/wN-<short-name>.js, copy tests/workflow.template/ → tests/wN-<short-name>/, and replace <WORKFLOW_PLACEHOLDER> markers with the workflow's short name.

Then install:

cd <target-dir> && npm install && npx playwright install chromium

3. Record each workflow

Per workflow:

  1. Fill in recordings/scripts/wN-<short-name>.js: user-action sequence, recordHar.urlFilter regex (allow-list the target host; block third-party RUM/ads — see references/recording-with-playwright.md), and a real Chrome userAgent (the default HeadlessChrome UA triggers bot-blocking on many sites).

  2. Run: node recordings/scripts/wN-<short-name>.js → writes recordings/har/wN-<short-name>.har

  3. Convert: npx har-to-k6 recordings/har/wN-<short-name>.har -o tests/wN-<short-name>/from-har.js

  4. Commit both HAR and from-har.js (audit trail for bundle-path changes).

If the recorder fails or produces an unusable HAR (bot-blocking, missing hydration, third-party noise), see the Recording section of references/gotchas.md and references/recording-with-playwright.md.

Prefer mcp-k6 recording and migration tools if available.

4. Build functional tests

Per workflow:

  1. Hand-clean from-har.js into protocol.js — drop per-request UA headers, rename groups, parameterise BASE_URL, replace session tokens, drop sleep(1), add expect() on every load-bearing response. Full procedure in references/functional-tests.md.

  2. Hand-write browser.js from the Playwright recorder using the 5-step procedure in references/functional-tests.md.

  3. Run ./tests/run-all.sh. Do not proceed to §5 until it exits 0.

Prefer mcp-k6 migration tools for Playwright→k6/browser conversion.

5. Design SLOs and thresholds

Adjust the opinionated defaults in assets/tests/workflow.template/ to the user's stated SLOs from §1. Four layers:

  1. Global SLOs — overall error rate + aggregate latency.

  2. Per-endpoint thresholds — tag every protocol request, threshold per tag.

  3. Per-iteration thresholds — workflow time + iteration completion rate.

  4. Web Vitals — LCP/INP/CLS only (no FCP).

Default globals:

http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
checks: ['rate>0.99'],

Per-endpoint tagging:

http.get(`${BASE_URL}/api/things`, { tags: { name: 'GetThings' } });
'http_req_duration{name:GetThings}': ['p(95)<400', 'p(99)<800'],

Web Vitals:

browser_web_vital_lcp: ['p(95)<2500'],
browser_web_vital_inp: ['p(95)<200'],
browser_web_vital_cls: ['p(95)<0.1'],

See references/slo-design.md for per-iteration tuning, the performance.mark custom-Trend pattern, iteration_completed Rate, breakpoint abort-on-fail thresholds, and loosening rules.

6. Build hybrid load tests

Per workflow, one file per test type. Each file has a protocol scenario (drives load) plus a single browser VU (measures Web Vitals under load). Breakpoint is protocol-only — a browser VU adds noise to the signal.

TypeExecutorDefaults
smokeconstant-vus3 VUs × 1m
averageramping-vus0→20→0 over 14m
stressramping-vus0→50→0 over 20m
spikeramping-vus0→100→0 over 2m
soakramping-vus0→10→0 over 70m
breakpointramping-arrival-rate5/s→500/s over 20m, abortOnFail

Tune per workflow once you've seen smoke results. See references/test-types.md for rationale and references/hybrid-load-design.md for why one file per type and why duplication between files is acceptable.

7. Run locally with LG sidecar

./tools/run-with-monitor.sh tests/wN-<short-name>/smoke.js

Starts lg-monitor.sh in the background, runs k6, then prints a summary verdict: OK (≥30% idle), NOTE (10–30%), or WARNING (<10%). If WARNING, the laptop is the bottleneck — reduce VUs, switch to cloud, or split across multiple LGs. See references/lg-monitoring.md.

8. Push to Grafana Cloud k6

For each test type assigned to cloud in the §1 runbook:

  1. Confirm k6 cloud login works (the skill does not own auth setup).

  2. Run smoke locally first to validate the script.

  3. k6 cloud run tests/wN-<short-name>/<type>.js

  4. Capture the run URL for the §10 report.

Cost reminder: browser VU-hours are billed 10× protocol VU-hours. Soak and breakpoint are the most expensive. Check limits before long runs. See references/local-vs-cloud.md.

9. Investigate the backend

Only if the user owns the backend and has Grafana access.

  1. Discover datasources via mcp-grafana or gcx datasources list.

  2. Ask the user for service label keys — do not guess.

  3. Correlate the k6 run window with RED metrics, error logs, traces, and profiles (Pyroscope — use explicit from/to for the run window).

  4. Hand back specific evidence: timestamps, query strings, panel links.

See references/grafana-investigation.md for the full flow including how to verify absence before reporting it.

10. Report back

Fill in the report template from references/reporting.md:

  • Summary — workflows, test types, dates

  • SLOs — pass/fail per threshold

  • Findings — one paragraph per finding, ordered by severity, with specific evidence

  • Evidence — k6 output paths, LG monitor CSVs, cloud run URLs, Grafana links

  • Suggested next steps

Always be specific. "Latency is high" is not a finding. "GetPizza p(95) hit 1.4s at iteration ~200; correlated with sustained 100% CPU on the recommender service per Grafana panel link" is.


Reference index

Files

references/gotchas.md

Generic gotchas

Things this skill has learned the hard way. All non-app-specific. None of these are framework / language / database gotchas — those are emergent findings, not assumptions.

Recording / functional

Third-party RUM and error reporters pollute HAR captures

Public websites typically embed third-party agents: RUM (Real User Monitoring), error reporters, ad networks, analytics, A/B test scripts, social-media widgets. Their requests:

  • Inflate the HAR by 10-100×.

  • Add unrelated endpoints to your per-endpoint threshold list.

  • Cause k6 to send test traffic to third-party collectors (rude and may breach their TOS).

Fix: recordHar.urlFilter regex that allow-lists only the target host(s). See recording-with-playwright.md.

HeadlessChrome UA triggers bot-blocking

Many production sites filter the default headless UA at the WAF or CDN level. Symptoms:

  • 403 / 429 / 503 on the first request.

  • Redirects to a bot-challenge page.

  • Empty responses.

Fix: set options.userAgent to a real Chrome UA on every k6 script, and browserContext({ userAgent: ... }) on every Playwright recorder.

Hydration timing breaks recorders

SPAs render some markup before client JS hydrates and attaches event handlers. If the recorder clicks before hydration, the click does nothing.

Fix: wait for a post-hydration sentinel locator (an interactive element that only exists after the framework has booted). See recording-with-playwright.md.

SLO design

http_req_duration p(95) < 500 hides slow endpoints

The global threshold is dominated by the fastest, most-frequent endpoints (static assets, small JSON). A slow critical endpoint hides in the aggregate.

Fix: tag every protocol request with tags: { name: '...' } and threshold per tag.

group() does not work in async (browser) code

k6's group() does not measure awaited code reliably. The group_duration metric will not capture what you intended.

Fix: use performance.mark/measure inside page.evaluate() and record a custom Trend. See slo-design.md.

Web Vitals thresholds may need loosening — but never silently

Some real apps have deterministic layout shifts (e.g. a widget that re-renders once data loads) the team has accepted. If you raise browser_web_vital_cls from < 0.1 to < 0.2 to make tests pass, report it: which workflow, which selector, the deterministic CLS value, and the customer's decision not to fix.

Silent loosening is the #1 way perf tests stop catching regressions.

Functional tests use expect(); load tests use check() + tags

expect() aborts iteration on failure. That's right for functional tests (single iteration, abort = failed test) but wrong for load tests (many iterations, abort poisons the run).

check() records a metric (checks rate) and continues. Pair it with tags: { name: '...' } on the underlying http call so failures show up per-endpoint in the summary.

In browser iterations use asyncCheck from k6-utils, not bare check()

The load-test templates import import { check as asyncCheck } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js' for assertions inside the browser scenario. This is deliberate.

Standard check() from k6 is synchronous: it evaluates each predicate immediately and expects a boolean back. Browser locator methods (isVisible(), textContent(), …) are async and return Promises. A Promise is always truthy, so a bare check() against an async predicate:

  • passes every time, regardless of the actual page state, and

  • never awaits the assertion, so the check is meaningless.

The iteration shows green while the assertion never really ran — the same silent-green-failure class as the library-mixing gotcha above. asyncCheck from k6-utils awaits the predicate before recording the result.

Rule: synchronous protocol code → check() from k6; async browser code → asyncCheck (check from k6-utils).

Pick one expect/check library per workflow — don't mix

The templates ship expect() from k6-testing (a JS library loaded over HTTPS — Jest-style: expect(r.status).toBe(200)) in protocol.js and browser.js, and bare check() from k6 in the load-test files (smoke.js, average.js, ...).

The exact import URL the templates use is:

import { expect } from 'https://jslib.k6.io/k6-testing/0.5.0/index.js';

Do NOT replace this with any of the following — they look similar but are different code and will break in stock k6:

Wrong importWhy it breaks
import { expect } from 'k6/x/expect'k6/x/* are xk6 extensions that must be compiled into a custom k6 binary. Stock k6 fails at provisioning.
import { expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'Different assertion style: .to.equal() chains. If you import this and use .toBe() syntax, every call TypeErrors.
import { expect } from '@playwright/test'Playwright lib; doesn't exist in k6's runtime.

When you adapt a template into a real workflow, stay with the library the template chose. Mixing libraries inside the same iteration produces silent test invalidation:

  • Importing expect from k6-testing and then calling .to.equal() (Chai/k6chaijs syntax) throws TypeError: Cannot read property 'equal' of undefined or null on every iteration.

  • k6 then registers zero checks for the iteration. The threshold checks: ['rate>0.99'] passes with rate=0.00% (k6 treats an empty metric as non-violating).

  • All check failures are masked. The smoke run appears green but the assertions never ran.

Rule: the import path determines the assertion API. k6-testing = .toBe() / .toContain() / .toEqual(). k6chaijs = .to.equal() / .to.include() / .to.deep.equal(). Don't cross-import.

Workflows with expected 4xx/5xx responses break the default http_req_failed threshold

Some workflows intentionally exercise the failure path — e.g. "anonymous user tries to rate a pizza, expect 401", "request a deleted resource, expect 404". k6 counts any 4xx/5xx as a failed request by default, so a workflow that produces one expected 401 out of every 5 requests will sit at a 20% http_req_failed rate and immediately blow the template's http_req_failed: ['rate<0.01'] threshold.

You have two correct fixes; pick one per workflow and document the choice in runbook.md:

Fix A — module-level: tell k6 the expected status is not a failure. This is the cleaner option when the failure path is part of the contract:

import http from 'k6/http';

// Treat HTTP 401 as a non-failure response for this workflow's
// anonymous-rate check; all other non-2xx still count as failed.
http.setResponseCallback(
  http.expectedStatuses({ min: 200, max: 299 }, 401),
);

Fix B — per-request: tag the expected-failure call and threshold its tag separately. Useful when only one specific request is the expected-failure case:

http.post(`${BASE_URL}/api/rate`, body, {
  tags: { name: 'AnonRateExpected401', expectedNon2xx: 'true' },
});

// thresholds:
'http_req_failed{expectedNon2xx:true}': ['rate<1.0'],     // permits 100% failed
'http_req_failed{expectedNon2xx:!true}': ['rate<0.01'],   // 2xx-only requests stay at <1%

Do not "fix" this by relaxing the global http_req_failed threshold from rate<0.01 to rate<0.25. That hides real failures elsewhere in the workflow.

Load generation

LG saturation masquerades as server slowness

A CPU-pegged laptop running k6 silently throttles its own request rate. Symptoms: lower-than-expected RPS, higher-than-expected latency, "iterations interrupted".

Fix: always run with run-with-monitor.sh. Treat WARNING verdicts (sys_cpu_idle < 10%) as inconclusive runs.

Browser VUs cost ~1 GB RSS each

Don't stack browser VUs to "drive more load". Use protocol VUs for load; keep browser VUs at 1 per scenario as a Web Vitals canary.

Custom Trend metrics accumulate in k6 client memory

Long browser soaks (60+ minutes) show k6 RSS climbing on the LG. This is not necessarily a server-side leak — k6 accumulates every metric sample in memory for the run summary.

Verify against the backend's memory metrics before reporting a leak. If backend memory is flat but k6 RSS climbs, the climb is client-side metric accumulation.

${ARRAY[-1]} is bash 4+; macOS ships bash 3.2

Negative-index array subscript syntax was added in bash 4. macOS ships bash 3.2 for licensing reasons. Helper scripts that use ${arr[-1]} will fail on macOS with bad array subscript.

Fix: explicit index arithmetic:

LAST_INDEX=$(( ${#arr[@]} - 1 ))
last="${arr[$LAST_INDEX]}"

Cloud k6

Browser VU-hours cost 10× protocol VU-hours

A 70-minute soak with 10 protocol + 1 browser VU costs: (10 × 70/60) + (1 × 70/60 × 10) = 11.67 + 11.67 = 23.3 VUh. The single browser VU costs as much as all 10 protocol VUs.

Confirm with the user before running browser-heavy soaks in cloud.

Local results may disagree with cloud results

Possible reasons (all legitimate):

  • LG was saturated locally.

  • Network path differs (CDN edge location).

  • Cloud cloud.distribution spreads VUs geographically.

  • IP allow-list / WAF treats cloud LG IPs differently.

Trust cloud for measurement, local for validation. Don't blend them.

k6 cloud login failures

If k6 cloud run fails with authentication errors, the user hasn't completed the Grafana Cloud k6 auth flow on this machine. Don't try to fix it inside this skill — pause and ask them to authenticate (typically k6 cloud login, or whatever their organisation's auth process is) before retrying.

Reporting

Vague findings are useless

"Latency is high" is not a finding. "GetRecommendation p(95) hit 1.4s at iteration ~200; corresponded to LG WARNING verdict; see log path X" is.

Every finding needs: a specific metric, a specific window, and a specific evidence path (k6 summary log, LG CSV, Grafana panel).

Don't fabricate backend findings from client-side signals

"The database is slow" cannot be concluded from k6 alone. You can say "the recommend endpoint's response time is high" — that's a client-side observable. "The database is slow" requires backend investigation per §9 of SKILL.md.

Don't recommend fixes

The customer's engineers know their code. The skill reports what k6 observed and what Grafana shows. It does not recommend code changes. Hand the evidence back and let the team decide.

Process

Always elicit workflows before scaffolding

Without explicit workflows, every later step is guesswork. The elicitation step exists for this reason; skipping it produces tests that measure the wrong thing.

Don't proceed to load testing until functional tests pass

./tests/run-all.sh must exit 0. A failing functional test means the script has a bug; running load against it produces noise, not signal.

One file per test type, not a dispatcher

Tempting to write load.js with LOAD_TYPE=__ENV.LOAD_TYPE. Reject the temptation. Each file should read cleanly in isolation during incident review. See hybrid-load-design.md.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Node.js

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Grafana Labs
Publisher
Grafana Labs (Official)
Source
grafana/skills · skills/grafana-k6/k6-perf-test-website · commit 8fb72a5
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Grafana Labs or Grafana Labs.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download k6-perf-test-website.zip