Load, Stress, and Benchmark Testing
Performance testing measures how a system behaves under load: k6 and Artillery simulate realistic traffic, ramp virtual users toward a breaking point, and fail the run against explicit thresholds. AI generates the scripts, reads the summary alongside the slow-query log to rank bottlenecks, and wires a regression gate into CI before a traffic spike finds the limit in production.
Your API handles 500 requests per second in staging and everyone celebrates. Then Black Friday hits, traffic spikes to 3,000 rps, and the database connection pool exhausts within minutes. p95 latency climbs from 120ms to 4s, the response-time graph looks like a hockey stick, and nobody can point to the change that did it.
What you need is a load test that reproduces the spike, a read on which layer saturates first, and a regression gate in CI so the next deploy does not surprise you at 9pm. This guide drives that loop with AI: generate a realistic script, run it, have the model read the output and the slow-query log, and wire a threshold gate into the pipeline. The same prompts work in Cursor, Claude Code, and Codex — the only thing that differs is how you invoke each tool.
What this performance-testing workflow gives you
Section titled “What this performance-testing workflow gives you”- The official k6 MCP server (
grafana/mcp-k6) wired into all three tools, plusk6 x agent initto bootstrap it in one command - Copy-paste prompts that generate runnable k6 scripts: a flash-sale ramp, a per-endpoint stress test that finds the breaking point, and a sub-60-second smoke test
- Two analysis prompts — one that reads a load-curve table, one that reads a k6 summary plus a Postgres slow-query log — and produces a ranked, fix-first bottleneck list
- A Playwright + Lighthouse prompt that measures Core Web Vitals (LCP, INP, CLS) under concurrent load
- Database benchmarks that compare a query with and without its index, at production row counts
- Two CI gates that catch different regressions: absolute thresholds via
k6 runexit codes, and a baseline comparison that fails a PR on a 20% p95 slide - The failure modes that make load-test numbers lie — and how to catch them before you trust the dashboard
Wiring up the tooling
Section titled “Wiring up the tooling”The piece worth installing on top of that is the official k6 MCP server from Grafana (grafana/mcp-k6). It lets the agent validate scripts, run tests, and read k6 docs without you copy-pasting CLI output back and forth. It ships as a Go binary or Docker image — not an npm package, so ignore any npx-based “k6 MCP” you find; those are unofficial wrappers.
The fastest path is k6 x agent init (requires k6 v2.0+ on your PATH), which drops the right skill files into your editor and registers the MCP server for whichever tool you’re using:
# Bootstrap k6 skills + MCP config for one editor...k6 x agent init claude-code # or cursor, codex
# ...or wire up every supported editor at oncek6 x agent init --allIf you’d rather wire the MCP server up by hand, the install is identical across tools — only the registration command differs.
Install the binary, then add it in Settings → MCP → Add with mcp-k6 as the command (stdio). Or drop this into .cursor/mcp.json:
{ "mcpServers": { "k6": { "command": "mcp-k6" } }}Install the binary first: brew tap grafana/grafana && brew install mcp-k6 (or docker pull grafana/mcp-k6:latest).
brew tap grafana/grafana && brew install mcp-k6claude mcp add --scope user --transport stdio k6 -- mcp-k6
# No local binary? Run it through Docker instead:claude mcp add --scope user --transport stdio k6 -- docker run --rm -i grafana/mcp-k6Add it to ~/.codex/config.toml:
[mcp_servers.k6]command = "mcp-k6"# or run via Docker:# command = "docker"# args = ["run", "--rm", "-i", "grafana/mcp-k6"]Install the binary with brew install mcp-k6 after brew tap grafana/grafana.
For browser-side performance you’ll also want the Playwright MCP (@playwright/mcp), which is the same setup in every tool:
claude mcp add playwright -- npx -y @playwright/mcp@latestIn Cursor add it via Settings → MCP; in Codex add a [mcp_servers.playwright] block pointing at the same npx command.
The loop: generate, run, analyze, gate
Section titled “The loop: generate, run, analyze, gate”The loop is the same in every tool. What changes is the invocation.
- Generate the script from a real endpoint, a target VU count, a ramp profile, and explicit thresholds — not “write a load test.”
- Run it against staging (
k6 run script.js), with the MCP server letting the agent execute and read output directly. - Analyze the results by handing the k6 summary plus the slow-query log to the model and asking for a ranked bottleneck list.
- Gate it in CI so a p95 regression fails the build before it ships.
Here is how you kick off steps 1 and 2 in each tool:
Open the agent panel (Cmd+I), paste a generation prompt, and let it write tests/load/checkout.js. With the k6 MCP server connected, the agent can run k6 run itself and iterate on threshold failures inline. Use a checkpoint before the run so you can roll the script back if the generated VU profile is unrealistic.
claude "Generate a k6 load test at tests/load/checkout.js for POST /api/checkout:ramp 0->200 VUs over 2m, hold 5m, ramp down 1m. Add thresholds:http_req_duration p95<300 p99<800, http_req_failed rate<0.01. Then run itagainst $STAGING_URL and summarize threshold pass/fail."With the k6 MCP server registered, Claude Code runs the test through the server and reads the summary back without you copy-pasting.
Run it as a one-shot from the CLI, or kick it off in Codex Cloud so the long-running test executes off your machine:
codex --sandbox workspace-write -c approval_policy=on-request \"Generate a k6 load test at tests/load/checkout.js for POST /api/checkout(ramp 0->200 VUs over 2m, hold 5m), add p95<300/p99<800 thresholds, run itagainst $STAGING_URL, and report which thresholds failed."For a trusted unattended CI run, use codex exec --sandbox workspace-write -c approval_policy=never and grant staging network access deliberately in the isolated CI configuration. never suppresses prompts but does not expand the sandbox; use least-privilege staging credentials and never point this job at production.
Generating the load test
Section titled “Generating the load test”The single-endpoint prompt is the one to reach for first: it produces a script you can read in a minute and trust, because every number in it is one you supplied.
A generated script should come out looking roughly like this — concrete VU stages and thresholds, not a happy-path skeleton:
import http from 'k6/http';import { check, sleep } from 'k6';
export const options = { stages: [ { duration: '2m', target: 200 }, // ramp to peak { duration: '5m', target: 200 }, // hold { duration: '1m', target: 0 }, // ramp down ], thresholds: { http_req_duration: ['p(95)<300', 'p(99)<800'], http_req_failed: ['rate<0.01'], },};
export default function () { const res = http.post(`${__ENV.STAGING_URL}/api/checkout`, JSON.stringify({ cartId: 'c_load_test', paymentMethod: 'pm_test', }), { headers: { 'Content-Type': 'application/json' } });
check(res, { 'status is 200': (r) => r.status === 200 }); sleep(1); // think time so 200 VUs != 200 RPS}k6 run exits non-zero when a threshold fails, which is exactly what makes the CI gate below trivial.
One endpoint is a start; a real flow is several, and the shape of the traffic matters as much as the volume. These three prompts scale the same recipe up — a multi-step user journey with a spike, a whole suite with helpers and npm scripts, and a repo-wide pass that picks the endpoints for you.
Generate a k6 load test for our checkout API:
Scenario: Simulate a flash sale with ramping traffic- Ramp from 0 to 100 virtual users over 2 minutes- Hold at 100 VUs for 5 minutes (steady state)- Spike to 500 VUs for 1 minute (flash sale moment)- Return to 100 VUs for 2 minutes (recovery)- Ramp down to 0 over 1 minute
API calls per virtual user iteration:1. POST /api/auth/login (use test credentials from env)2. GET /api/products?category=sale (browse sale items)3. POST /api/cart/items (add random product)4. POST /api/checkout (complete purchase with test payment)
Thresholds:- p95 response time < 500ms during steady state- p99 response time < 2000ms during spike- Error rate < 1% at all times- Checkout success rate > 99%
Save to /tests/performance/checkout-load.k6.jsclaude "Create a comprehensive k6 performance test suite:
1. /tests/performance/checkout-load.k6.js - Checkout flow load test - Ramping traffic pattern: 0 -> 100 -> 500 -> 100 -> 0 VUs - Realistic user journey (login, browse, cart, checkout) - SLA thresholds for response time and error rate
2. /tests/performance/api-stress.k6.js - API endpoint stress test - Test each critical endpoint individually - Find the breaking point (ramp until errors > 5%) - Report max throughput per endpoint
3. /tests/performance/helpers/auth.js - Shared auth helper - Login and cache tokens - Token refresh handling
4. package.json scripts: - test:perf:load - Run load tests - test:perf:stress - Run stress tests - test:perf:smoke - Quick 30-second smoke test
Include realistic test data generation for each scenario."Create a performance testing suite for this project:1. Analyze the API routes to identify critical endpoints2. Generate k6 load tests for the top 5 most important flows3. Create stress tests that find breaking points4. Add performance smoke tests for CI integration5. Create a PR with the test suite and documentation
Include realistic traffic patterns based on typical SaaS usage.Stress testing: finding the breaking point
Section titled “Stress testing: finding the breaking point”A load test asks “does it hold at the traffic we expect?” A stress test asks “at what number does it stop holding?” — and the answer is what goes in the capacity-planning doc. Define the breaking point before the run, so the test reports a number instead of an impression.
Analyzing the results
Section titled “Analyzing the results”A red threshold tells you something is slow. It does not tell you which layer saturated, and that is the question worth handing to a model. Two prompts, depending on what evidence you have.
When all you have is the load curve, the shape of the degradation is itself the evidence — a clean linear climb points somewhere very different from a cliff:
When you can also pull the database’s slow-query log from the same window, the diagnosis stops being inference. This second prompt demands evidence for every claim and ends in one prioritized change rather than a list of possibilities:
To go further, connect the Sentry MCP so the agent can correlate threshold failures with the errors and slow transactions Sentry recorded during the run. The remote server is the simplest setup and is identical across tools:
# Official Sentry remote MCP (recommended)claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
# Local stdio alternative, if you need itclaude mcp add sentry -- npx -y @sentry/mcp-serverThen ask: “For the load-test window 14:00–14:08 UTC, pull the slowest transactions and any new errors from Sentry and line them up against the k6 threshold failures.” That turns a red CI run into a specific list of transactions to fix.
Core Web Vitals under concurrent load
Section titled “Core Web Vitals under concurrent load”Server-side p95 can be healthy while the page still feels broken, because the browser is doing work the API never sees. Measure the front end while the API is under load, not on an idle box.
Database performance benchmarks
Section titled “Database performance benchmarks”Most load-test cliffs turn out to be a query that was fine at seed-data scale. Benchmark the queries directly, at production row counts, with and without the index you are arguing about.
Gating regressions in CI
Section titled “Gating regressions in CI”Two gates catch two different failures, and you want both. The first is absolute: the script’s own thresholds define “too slow”, and because k6 run returns a non-zero exit code when any threshold is breached, the CI step is just running the script — no custom comparison logic. Use the official grafana/setup-k6-action, not a hand-rolled curl install:
- uses: actions/checkout@v5- uses: grafana/setup-k6-action@v1- run: k6 run tests/load/checkout.js env: STAGING_URL: ${{ secrets.STAGING_URL }} TOKEN: ${{ secrets.LOAD_TEST_TOKEN }}The second gate is relative, and it catches what the first cannot: a change that doubles p95 from 90ms to 180ms passes a p(95)<300 threshold while quietly eating half your headroom. For that you need a stored baseline and a percentage comparison.
Load and stress runs are long and CPU-hungry, so where you run them matters as much as the script. Each tool has a natural home for k6 jobs:
Author and debug the k6 scripts locally in Agent mode against a staging URL, then commit the workflow file. Cursor is where you iterate on thresholds and scenarios; you do not want long stress runs blocking the editor, so keep the in-editor runs to the 30-second smoke test.
Run the smoke benchmark headlessly as a PR gate: claude -p "run k6 run tests/performance/smoke.k6.js, compare p95 against perf-baseline.json, and fail if it regressed more than 20%" inside the GitHub Actions job. Pair it with a PostToolUse hook so the comparison comment is posted automatically.
Offload the long load and stress runs to Codex Cloud or a scheduled automation so they execute on cloud hardware, not the PR runner — then have it open a PR (or comment) with the results table. This keeps multi-minute runs off the critical CI path while still gating merges on the cloud result.
The practical split is tiered: the 30-second smoke test on every PR, the full load test nightly, the stress test weekly. The smoke test catches the obvious regressions; the longer runs catch the subtle ones without holding up a merge.
When load-test numbers lie
Section titled “When load-test numbers lie”Load-test numbers lie in predictable ways. These are the failure modes that send teams chasing the wrong fix.
When a result looks impossible, hand the full k6 summary and the generator’s resource metrics to your AI tool and ask it to distinguish a real server bottleneck from a test artifact before you escalate.