performance

Microbench a health handler

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)

Testing tool

Autocannon · HTTP microbenchmark (Node) · MIT

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`);