Skip to content

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, plus k6 x agent init to 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 run exit 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

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:

Terminal window
# Bootstrap k6 skills + MCP config for one editor...
k6 x agent init claude-code # or cursor, codex
# ...or wire up every supported editor at once
k6 x agent init --all

If 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).

For browser-side performance you’ll also want the Playwright MCP (@playwright/mcp), which is the same setup in every tool:

Terminal window
claude mcp add playwright -- npx -y @playwright/mcp@latest

In Cursor add it via Settings → MCP; in Codex add a [mcp_servers.playwright] block pointing at the same npx command.

The loop is the same in every tool. What changes is the invocation.

  1. Generate the script from a real endpoint, a target VU count, a ramp profile, and explicit thresholds — not “write a load test.”
  2. Run it against staging (k6 run script.js), with the MCP server letting the agent execute and read output directly.
  3. Analyze the results by handing the k6 summary plus the slow-query log to the model and asking for a ranked bottleneck list.
  4. 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.

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.js

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.

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:

Terminal window
# Official Sentry remote MCP (recommended)
claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
# Local stdio alternative, if you need it
claude mcp add sentry -- npx -y @sentry/mcp-server

Then 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.

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.

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.

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:

.github/workflows/load-test.yml
- 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.

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.

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.