Load Testing with k6 skill for Codex
Answer a capacity question, not just draw a chart. Codex starts every load test from a written question and a traffic model, then writes k6 scenarios using open-model arrival rates, tagged endpoints and thresholds that fail the run. It checks that the load generator kept up before trusting results, correlates them with server metrics, and refuses to point traffic at shared or production systems without explicit confirmation.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- load-testing Installs to ~/.agents/skills/load-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install load-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project load-testing Installs to .agents/skills/load-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/load-testing.zipunzip -o load-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) load-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $load-testing, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Plans and writes k6 load tests - smoke, average-load, stress, spike, soak and breakpoint profiles, open-model arrival-rate scenarios, thresholds on p95/p99 latency and error rate that fail CI, tagged endpoints, data parameterization with SharedArray, and result interpretation including load generator saturation. Use when asked how many users or requests a service can handle, to load test an API before launch, to add a performance gate to CI, or to reproduce slowdowns that only appear under traffic.
Use it when
- Estimating capacity before a launch or traffic event
- Adding a performance regression gate to CI
- Reproducing slowdowns that only appear under concurrency
- Reviewing an existing k6 script or its results
Use something else when
- You need to time a single function precisely: use benchmark-testing
- The slowdown is already reproducible and needs profiling: use node-profiling
How to check the result
The run shows zero dropped iterations and moderate generator CPU, thresholds are evaluated and reflected in the exit code, and results line up with server-side metrics for the same window.
The mistake it prevents
Using a closed VU model with no think time against one cached URL, which hides slowdowns through coordinated omission and measures the cache instead of the system.
A load test is worth running when it answers a written question (“does checkout keep p95 under 400 ms at 150 requests per second for 30 minutes?”), uses a traffic model that matches production, fails automatically on thresholds, and its results are trusted because the load generator was not the bottleneck.
Safety first
- Never load test production or a shared environment without explicit user confirmation that names the target, peak rate and time window. Third-party APIs, payment providers and email services called by the target must be stubbed or in sandbox mode.
- Running k6 against a remote host needs network access, which may require approval
in the Codex sandbox. Local smoke runs against
localhostdo not. - Warn the owners of WAF, rate limiting and autoscaling: a test can trigger bans or scale-ups that cost money.
Inspect first
- Read
AGENTS.mdand any existingload/,perf/ork6/folder. Reuse helpers. - Gather the target numbers: current peak RPS from metrics, expected growth, latency SLOs. If none exist, ask; a load test without a target produces a chart, not a decision.
- Identify the critical journeys and their traffic mix from analytics or access logs (for example 70% browse, 20% search, 10% checkout).
- Check
k6 version. k6 1.x runs TypeScript files directly and keeps the scripting API below.
Choose the test type
| Type | Question | Shape |
|---|---|---|
| Smoke | Does the script work at all? | 1-2 VUs, 1 minute; run on every change |
| Average load | Does it meet SLOs at normal peak? | ramp to expected peak, hold 15-30 min |
| Stress | How does it degrade above peak? | step up 150%, 200%, 300% of peak |
| Spike | Does it survive a sudden burst and recover? | jump to many times peak in seconds, then drop |
| Soak | Leaks, connection exhaustion, slow drift? | average load for hours |
| Breakpoint | Where does it fall over? | ramp arrival rate until thresholds abort |
Run smoke first, always. Then average load. Only then stress, spike or breakpoint.
Open vs closed model
- Closed model (
constant-vus,ramping-vus): each VU waits for a response before sending the next request. When the system slows down, load drops, which hides the problem (coordinated omission). - Open model (
constant-arrival-rate,ramping-arrival-rate): new iterations start at a fixed rate regardless of response time, like real users arriving. Use this for APIs and anything with an RPS target.
Default to arrival-rate executors for services; use VU-based executors for session-style flows where each user does a sequence with think time.
Script template
// load/checkout.js
import http from "k6/http";
import { check, sleep } from "k6";
import { SharedArray } from "k6/data";
const BASE = __ENV.BASE_URL || "http://localhost:8080";
const users = new SharedArray("users", () => JSON.parse(open("./data/users.json")));
export const options = {
scenarios: {
browse: {
executor: "ramping-arrival-rate",
exec: "browse",
startRate: 10,
timeUnit: "1s",
preAllocatedVUs: 50,
maxVUs: 300,
stages: [
{ target: 105, duration: "5m" }, // ramp to 70% of 150 rps
{ target: 105, duration: "20m" }, // hold
{ target: 0, duration: "2m" },
],
},
checkout: {
executor: "ramping-arrival-rate",
exec: "checkout",
startRate: 1,
timeUnit: "1s",
preAllocatedVUs: 20,
maxVUs: 100,
stages: [
{ target: 15, duration: "5m" },
{ target: 15, duration: "20m" },
{ target: 0, duration: "2m" },
],
},
},
thresholds: {
http_req_failed: ["rate<0.01"],
"http_req_duration{scenario:browse}": ["p(95)<300", "p(99)<800"],
"http_req_duration{name:POST /orders}": [{ threshold: "p(95)<600", abortOnFail: true, delayAbortEval: "1m" }],
checks: ["rate>0.99"],
dropped_iterations: ["count<100"],
},
};
export function browse() {
const res = http.get(`${BASE}/api/products?page=${Math.ceil(Math.random() * 20)}`, {
tags: { name: "GET /api/products" },
});
check(res, { "browse 200": (r) => r.status === 200 });
}
export function checkout() {
const user = users[Math.floor(Math.random() * users.length)];
const res = http.post(`${BASE}/orders`, JSON.stringify({ sku: "LAMP-01", qty: 1 }), {
headers: { "Content-Type": "application/json", Authorization: `Bearer ${user.token}` },
tags: { name: "POST /orders" },
});
check(res, { "order created": (r) => r.status === 201 });
sleep(1);
}
Key points:
- Tag requests with a stable
nameso URLs with ids aggregate into one metric instead of thousands of series. dropped_iterationsabove zero means k6 could not start iterations on schedule: raisemaxVUsor the system is too slow; either way the target rate was not tested.abortOnFailstops a stress test early once it is clearly failing, saving time and sparing the environment.- Use
setup()to create shared fixtures (tokens) once; its return value is passed to every iteration. open()works only in the init context (top level), not inside functions.- Keep a
load/smoke.jsthat re-exports the scenario functions (export { browse, checkout } from "./checkout.js";) with its own smalloptions(oneper-vu-iterationsscenario per function). Running the full script with--vus/--durationreplaces the scenarios and calls adefaultfunction that this script does not have.
Running
k6 run -e BASE_URL=http://localhost:8080 load/checkout.js # full run
k6 run load/smoke.js # 1 VU, few iterations
k6 run --summary-export=build/k6-summary.json load/checkout.js
K6_WEB_DASHBOARD=true k6 run load/checkout.js # live dashboard on :5665
k6 run --out json=build/k6-raw.json load/checkout.js # raw points, large
k6 exits non-zero when any threshold fails (exit code 99), which is what CI should key on.
CI gate
Run a short, low-rate version on PRs (smoke plus a 3-5 minute average-load slice against an ephemeral environment) and the full profile nightly or before releases. Thresholds in the PR run must be loose enough to avoid noise on shared runners; tight SLO thresholds belong to the dedicated runs.
Reading results
- Validate the test before the system: check
dropped_iterations, k6’s own CPU (keep the load generator under about 80% CPU; otherwise split across machines), and the network between generator and target. - Error rate first, then latency percentiles. A fast p95 with 5% errors is a failing run.
- Correlate with server metrics for the same window: CPU, memory, GC pauses, DB connections and slow queries, queue depth, pod restarts, autoscaling events.
- Find the knee: the arrival rate where p99 starts climbing faster than linearly. That, not the crash point, is usable capacity.
- Compare to the previous run with the same script and data; report differences, not absolute numbers alone.
Gotchas
- Testing one cached URL measures the cache. Parameterize ids and queries.
- Load from a single IP can hit per-IP rate limits and WAF rules; whitelist or distribute.
- Ignoring
checkslets 200 responses with error bodies count as success; check content. - Ramping VUs without think time models no real user; add
sleepfor session flows. - Averages hide tail latency; never report a mean without p95 and p99.
- Production-like data volume matters; an empty database answers in microseconds.
Output
**Question:** Can checkout sustain 150 rps with p95 < 400 ms?
**Profile:** ramping-arrival-rate, 70/20/10 mix, 5 min ramp, 20 min hold, staging (3 pods)
**Result:** PASS up to 150 rps (p95 310 ms, p99 720 ms, errors 0.2%).
Knee at ~190 rps: p99 2.4 s, DB connection pool saturated (max 20).
**Generator health:** 0 dropped iterations, k6 CPU 45%
**Recommendation:** raise pool to 40 and retest at 220 rps before launch
**Artifacts:** build/k6-summary.json, Grafana dashboard link for the window What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Load Testing with k6"
short_description: "Answer a capacity question, not just draw a chart"
default_prompt: "$load-testing Write a k6 test to check our API handles 200 requests per second with p95 under 300 ms" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.