{"id":"ops/k6-slo","relativePath":"ops/k6-slo.md","title":"k6 SLO Load Test (SOTA §20.4)","markdown":"# k6 SLO Load Test (SOTA §20.4)\n\nThis runbook validates the key p95 latency targets from `LinkedArtSOTAWebApp.md` §20.4:\n\n- API cached `Record` hit p95 `< 200ms`\n- API cold `Record` read p95 `< 500ms`\n- Search keyword+facet-style query p95 `< 300ms`\n- Whitelisted SPARQL query p95 `< 2s`\n- IIIF tile serving p95 `< 100ms`\n\n## Command\n\n```bash\npnpm k6:slo\n```\n\n`pnpm k6:slo` writes a machine-readable summary to:\n\n- `artifacts/performance/k6-slo-summary.json`\n\n## What it exercises\n\nThe k6 script (`scripts/k6-slo.js`) runs five scenarios:\n\n1. `cached_record_hit`\n   - Repeated GET on one warmed `recordId`:\n   - `GET /api/records/:id`\n2. `cold_record_read`\n   - Rotating `recordId` reads with cache-busting query/header hints:\n   - `GET /api/records/:id?cache_bust=...`\n3. `keyword_facet_search`\n   - Keyword + type filter query:\n   - `GET /api/search?q=...&type=HumanMadeObject&limit=25&offset=0`\n4. `sparql_whitelisted_query`\n   - Read-only whitelisted SPARQL query:\n   - `POST /api/getty/sparql` with `{ \"query\": \"SELECT * WHERE { ?s ?p ?o } LIMIT 1\" }`\n5. `iiif_tile_serving`\n   - CDN-backed IIIF tile or production tile surrogate:\n   - `GET ${IIIF_TILE_URL}`\n\nThresholds are enforced per scenario; non-compliance exits non-zero.\n\n## Tuning knobs\n\nEnvironment variables:\n\n- `BASE_URL` (default: `http://localhost:3000`)\n- `METAMUSEUM_K6_RATE` (default: `16` iterations/sec/scenario)\n- `METAMUSEUM_K6_DURATION` (default: `45s` per scenario)\n- `METAMUSEUM_K6_PREALLOCATED_VUS` (default: `8`)\n- `METAMUSEUM_K6_MAX_VUS` (default: `24`)\n- `SEARCH_TYPE` (default: `HumanMadeObject`)\n- `SEARCH_QUERY` (default auto-derived from first record label, fallback `art`)\n- `SPARQL_URL` (default: `${BASE_URL}/api/getty/sparql`)\n- `SPARQL_QUERY` (default: `SELECT * WHERE { ?s ?p ?o } LIMIT 1`)\n- `IIIF_TILE_URL` (default: `${BASE_URL}/icon.jpg`; set this to a CDN-backed IIIF tile URL for production evidence)\n\nExample:\n\n```bash\nMETAMUSEUM_K6_RATE=24 METAMUSEUM_K6_DURATION=60s pnpm k6:slo\n```\n\nProduction-style evidence should set `IIIF_TILE_URL` to a CDN-backed tile and may set `SPARQL_URL`/`SPARQL_QUERY` to the whitelisted query route used for the deployment under test.\n\n## Runner behavior\n\n`scripts/k6-slo-runner.mjs` picks the first available runtime in this order:\n\n1. Local workspace binary (`.tools/k6/**/k6.exe`)\n2. `k6` on PATH\n3. Docker image `grafana/k6:latest` (maps `localhost` → `host.docker.internal`)\n\nThis keeps execution portable across local dev setups and CI agents.\n","sections":[{"level":2,"heading":"Command","anchor":"command"},{"level":2,"heading":"What it exercises","anchor":"what-it-exercises"},{"level":2,"heading":"Tuning knobs","anchor":"tuning-knobs"},{"level":2,"heading":"Runner behavior","anchor":"runner-behavior"}],"html":"<h1 id=\"k6-slo-load-test-sota-20-4\">k6 SLO Load Test (SOTA §20.4)</h1>\n<p>This runbook validates the key p95 latency targets from `LinkedArtSOTAWebApp.md` §20.4:</p>\n<ul><li>API cached `Record` hit p95 `&lt; 200ms`</li><li>API cold `Record` read p95 `&lt; 500ms`</li><li>Search keyword+facet-style query p95 `&lt; 300ms`</li><li>Whitelisted SPARQL query p95 `&lt; 2s`</li><li>IIIF tile serving p95 `&lt; 100ms`</li></ul>\n<h2 id=\"command\">Command</h2>\n<pre><code>\npnpm k6:slo\n</code></pre>\n<p>`pnpm k6:slo` writes a machine-readable summary to:</p>\n<ul><li>`artifacts/performance/k6-slo-summary.json`</li></ul>\n<h2 id=\"what-it-exercises\">What it exercises</h2>\n<p>The k6 script (`scripts/k6-slo.js`) runs five scenarios:</p>\n<ol><li>`cached_record_hit`</li></ol>\n<ol><li>`cold_record_read`</li></ol>\n<ol><li>`keyword_facet_search`</li></ol>\n<ol><li>`sparql_whitelisted_query`</li></ol>\n<ol><li>`iiif_tile_serving`</li></ol>\n<ul><li>Repeated GET on one warmed `recordId`:</li><li>`GET /api/records/:id`</li><li>Rotating `recordId` reads with cache-busting query/header hints:</li><li>`GET /api/records/:id?cache_bust=...`</li><li>Keyword + type filter query:</li><li>`GET /api/search?q=...&amp;type=HumanMadeObject&amp;limit=25&amp;offset=0`</li><li>Read-only whitelisted SPARQL query:</li><li>`POST /api/getty/sparql` with `{ &quot;query&quot;: &quot;SELECT * WHERE { ?s ?p ?o } LIMIT 1&quot; }`</li><li>CDN-backed IIIF tile or production tile surrogate:</li><li>`GET ${IIIF_TILE_URL}`</li></ul>\n<p>Thresholds are enforced per scenario; non-compliance exits non-zero.</p>\n<h2 id=\"tuning-knobs\">Tuning knobs</h2>\n<p>Environment variables:</p>\n<ul><li>`BASE_URL` (default: `http://localhost:3000`)</li><li>`METAMUSEUM_K6_RATE` (default: `16` iterations/sec/scenario)</li><li>`METAMUSEUM_K6_DURATION` (default: `45s` per scenario)</li><li>`METAMUSEUM_K6_PREALLOCATED_VUS` (default: `8`)</li><li>`METAMUSEUM_K6_MAX_VUS` (default: `24`)</li><li>`SEARCH_TYPE` (default: `HumanMadeObject`)</li><li>`SEARCH_QUERY` (default auto-derived from first record label, fallback `art`)</li><li>`SPARQL_URL` (default: `${BASE_URL}/api/getty/sparql`)</li><li>`SPARQL_QUERY` (default: `SELECT * WHERE { ?s ?p ?o } LIMIT 1`)</li><li>`IIIF_TILE_URL` (default: `${BASE_URL}/icon.jpg`; set this to a CDN-backed IIIF tile URL for production evidence)</li></ul>\n<p>Example:</p>\n<pre><code>\nMETAMUSEUM_K6_RATE=24 METAMUSEUM_K6_DURATION=60s pnpm k6:slo\n</code></pre>\n<p>Production-style evidence should set `IIIF_TILE_URL` to a CDN-backed tile and may set `SPARQL_URL`/`SPARQL_QUERY` to the whitelisted query route used for the deployment under test.</p>\n<h2 id=\"runner-behavior\">Runner behavior</h2>\n<p>`scripts/k6-slo-runner.mjs` picks the first available runtime in this order:</p>\n<ol><li>Local workspace binary (`.tools/k6/**/k6.exe`)</li></ol>\n<ol><li>`k6` on PATH</li></ol>\n<ol><li>Docker image `grafana/k6:latest` (maps `localhost` → `host.docker.internal`)</li></ol>\n<p>This keeps execution portable across local dev setups and CI agents.</p>","updatedAt":"2018-10-20T01:46:40.000Z","checksum":"328b5b3163d4f0f5f38659b837a2c71f426c78fc25093daddfc84f3ec257171d","checksumPrefix":"328b5b3163d4","anchorCount":4,"lineCount":75,"rawUrl":"/api/docs/content?path=ops%2Fk6-slo.md","htmlUrl":"/docs?doc=ops%2Fk6-slo.md","apiUrl":"/api/docs/content?path=ops%2Fk6-slo.md"}