performance

Smoke-load an HTTP GET

The smallest useful load test: start samples/js-api, apply a short smoke of concurrent GETs, and fail the run if the error rate or p95 latency crosses a threshold. Compare k6 and Artillery on the same contract.

SUT: js-api · id: perf.http-smoke-load

Cached CI results from 10/4/2026, 3:02:04 AM (ci · ff16d82)

Testing tool

k6 · Load / performance test runner (Grafana k6 OSS) · AGPL-3.0

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.

This is not a unit or integration assertion on one response. A load generator opens several virtual users against a real port, records latency and status codes, then evaluates thresholds. Duration stays short (~8s) so CI stays cheap — the idea is “does the handler survive a puff of traffic?”, not a soak.

SUT: js-api · samples/js-api/src/app.js · run npm test in examples/perf/http-smoke-load/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/http-smoke-load/k6/smoke.js · AGPL-3.0 · run in examples/perf/http-smoke-load/k6: npm test
// k6 smoke load — a few VUs, short duration, fail the process on thresholds
import http from "k6/http";
import { check, sleep } from "k6";

const BASE = __ENV.BASE_URL || "http://127.0.0.1:4180";

export const options = {
  vus: 5,
  duration: "8s",
  thresholds: {
    // Contract: almost no failed requests, p95 stays under half a second
    http_req_failed: ["rate<0.01"],
    http_req_duration: ["p(95)<500"],
  },
};

export default function () {
  const res = http.get(`${BASE}/items/1`);
  check(res, {
    "status is 200": (r) => r.status === 200,
    "body is Notebook": (r) => r.json("name") === "Notebook",
  });
  sleep(0.2);
}

Needs the k6 binary on PATH (brew install k6; CI uses grafana/setup-k6-action).