A handler microbench, not a multi-minute soak: open several connections to GET /health and treat p99 plus non-2xx as the pass/fail contract. Autocannon and k6 express the same burst.
SUT: js-api · id: perf.microbench-handler
Cached CI results from 10/3/2026, 10:14:14 AM (ci · d7784ca)
Autocannon is a small Node load generator in the wrk tradition: N connections, D seconds, print a latency table. We wrap it in a short script so p99 and non-2xx become a failing exit code, not just a pretty report.
The script calls autocannon() against GET /health, prints the default summary, then checks 2xx > 0, errors === 0, and p99 < 500ms. That last check is the test — raw RPS is informational and will vary on CI.
Testing architecture
Performance and load tests ask how the SUT behaves under concurrent HTTP, not whether one response matches a JSON fixture. samples/js-api is started on a real port; k6, Artillery, or Autocannon generate traffic; thresholds on errors and high-percentile latency fail the run. Duration stays short so CI stays cheap.
The SUT is still samples/js-api, but the question is “how does this one handler behave under a short burst?” rather than a VU smoke over /items/1. Connections stay open and fire as fast as the client allows. Thresholds are generous so CI machines do not flake; the teaching point is the threshold, not a trophy RPS number.
SUT: js-api · samples/js-api/src/app.js · run npm test in examples/perf/microbench-handler/autocannon
Code under test · samples/js-api/src/app.js
import express from "express";
/** In-memory catalog — no DB so integration tests stay local and cheap. */
const ITEMS = new Map([["1", { id: "1", name: "Notebook" }]]);
/**
* Build the Express app (no listen). Tests import this and bind a port themselves.
*/
export function createApp() {
const app = express();
// Parse JSON bodies if a later POST example needs them
app.use(express.json());
// Baseline browser-isolation headers — asserted by security.http-headers
app.use((_req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
next();
});
// Liveness probe — integration + load/microbench examples hit this
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
// Read one item by id from the in-memory store
app.get("/items/:id", (req, res) => {
const item = ITEMS.get(req.params.id);
if (!item) {
// Stable error contract: same JSON shape for every missing id
res.status(404).json({ error: "not_found", id: req.params.id });
return;
}
res.json(item);
});
return app;
}
Test · examples/perf/microbench-handler/autocannon/bench.mjs · MIT
· run in examples/perf/microbench-handler/autocannon: npm test
// Autocannon microbench — burst GET /health, then fail the process on the contract
import autocannon from "autocannon";
const url = process.env.BASE_URL || "http://127.0.0.1:4181/health";
const result = await autocannon({
url,
connections: 10,
duration: 5,
pipelining: 1,
});
autocannon.printResult(result);
const p99 = result.latency.p99;
const errors = (result.errors ?? 0) + (result.non2xx ?? 0) + (result.timeouts ?? 0);
if ((result["2xx"] ?? 0) < 1) {
console.error("Expected at least one 2xx response");
process.exit(1);
}
if (errors > 0) {
console.error(`Expected zero errors/non-2xx/timeouts, got ${errors}`);
process.exit(1);
}
// Generous p99 so shared CI CPUs do not flake — the threshold is the lesson
if (p99 > 500) {
console.error(`Expected p99 < 500ms, got ${p99}ms`);
process.exit(1);
}
console.log(`ok: 2xx=${result["2xx"]} p99=${p99}ms`);
k6 is a Go binary that runs JS scripts: virtual users, http.get, check(), and thresholds that fail the process. The OSS CLI is enough here — no Grafana Cloud. Install via Homebrew or the CI setup-k6 action.
start-server-and-test boots samples/js-api. k6 run then spins VUs that issue real HTTP. Thresholds (error rate, p95/p99) are the assertions — if they breach, the exit code is non-zero, same as a failed unit test.
Testing architecture
Performance and load tests ask how the SUT behaves under concurrent HTTP, not whether one response matches a JSON fixture. samples/js-api is started on a real port; k6, Artillery, or Autocannon generate traffic; thresholds on errors and high-percentile latency fail the run. Duration stays short so CI stays cheap.
The SUT is still samples/js-api, but the question is “how does this one handler behave under a short burst?” rather than a VU smoke over /items/1. Connections stay open and fire as fast as the client allows. Thresholds are generous so CI machines do not flake; the teaching point is the threshold, not a trophy RPS number.
SUT: js-api · samples/js-api/src/app.js · run npm test in examples/perf/microbench-handler/k6
Code under test · samples/js-api/src/app.js
import express from "express";
/** In-memory catalog — no DB so integration tests stay local and cheap. */
const ITEMS = new Map([["1", { id: "1", name: "Notebook" }]]);
/**
* Build the Express app (no listen). Tests import this and bind a port themselves.
*/
export function createApp() {
const app = express();
// Parse JSON bodies if a later POST example needs them
app.use(express.json());
// Baseline browser-isolation headers — asserted by security.http-headers
app.use((_req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
next();
});
// Liveness probe — integration + load/microbench examples hit this
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
// Read one item by id from the in-memory store
app.get("/items/:id", (req, res) => {
const item = ITEMS.get(req.params.id);
if (!item) {
// Stable error contract: same JSON shape for every missing id
res.status(404).json({ error: "not_found", id: req.params.id });
return;
}
res.json(item);
});
return app;
}
Test · examples/perf/microbench-handler/k6/bench.js · AGPL-3.0
· run in examples/perf/microbench-handler/k6: npm test
// k6 microbench — same /health burst + error / p99 contract as Autocannon
import http from "k6/http";
import { check } from "k6";
const BASE = __ENV.BASE_URL || "http://127.0.0.1:4181";
export const options = {
vus: 10,
duration: "5s",
thresholds: {
http_req_failed: ["rate<0.01"],
http_req_duration: ["p(99)<500"],
},
};
export default function () {
const res = http.get(`${BASE}/health`);
check(res, {
"status is 200": (r) => r.status === 200,
"ok is true": (r) => r.json("ok") === true,
});
}
Needs the k6 binary on PATH (brew install k6; CI uses grafana/setup-k6-action).