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.

  • Testing & QA
  • $load-testing
  • 194 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- load-testing

Installs to ~/.agents/skills/load-testing/ for every project on this machine.

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

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.

~/.agents/skills/load-testing/SKILL.md

--- name: load-testing description: Plans and writes k6 load tests - smoke, average-load, stress, spike, soak and breakpoint profiles, open-model arrival-rate scenarios, thresholds on… license: MIT ---

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 localhost do not.
  • Warn the owners of WAF, rate limiting and autoscaling: a test can trigger bans or scale-ups that cost money.

Inspect first

  1. Read AGENTS.md and any existing load/, perf/ or k6/ folder. Reuse helpers.
  2. 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.
  3. Identify the critical journeys and their traffic mix from analytics or access logs (for example 70% browse, 20% search, 10% checkout).
  4. Check k6 version. k6 1.x runs TypeScript files directly and keeps the scripting API below.

Choose the test type

TypeQuestionShape
SmokeDoes the script work at all?1-2 VUs, 1 minute; run on every change
Average loadDoes it meet SLOs at normal peak?ramp to expected peak, hold 15-30 min
StressHow does it degrade above peak?step up 150%, 200%, 300% of peak
SpikeDoes it survive a sudden burst and recover?jump to many times peak in seconds, then drop
SoakLeaks, connection exhaustion, slow drift?average load for hours
BreakpointWhere 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 name so URLs with ids aggregate into one metric instead of thousands of series.
  • dropped_iterations above zero means k6 could not start iterations on schedule: raise maxVUs or the system is too slow; either way the target rate was not tested.
  • abortOnFail stops 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.js that re-exports the scenario functions (export { browse, checkout } from "./checkout.js";) with its own small options (one per-vu-iterations scenario per function). Running the full script with --vus/--duration replaces the scenarios and calls a default function 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

  1. 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.
  2. Error rate first, then latency percentiles. A fast p95 with 5% errors is a failing run.
  3. Correlate with server metrics for the same window: CPU, memory, GC pauses, DB connections and slow queries, queue depth, pod restarts, autoscaling events.
  4. Find the knee: the arrival rate where p99 starts climbing faster than linearly. That, not the crash point, is usable capacity.
  5. 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 checks lets 200 responses with error bodies count as success; check content.
  • Ramping VUs without think time models no real user; add sleep for 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem