Sizing & load tests
Storage — the sizing question this page does NOT answer
The harness below measures throughput and latency, not disk. The disk story, in three facts:
- Nothing purges the ingested data. The source mirror (
connector.t_*), the RAG corpus (rag.chunk) and the run history grow until a connector is deleted (which purges its tables) or you clean up manually — no retention policy exists. - The one automatic purge is the event outbox: daily at 04:30, 30-day window
(
lumnik.events.retention-days), never past the slowest consumer. - No disk gauge ships:
/q/metricscarries JVM/HTTP/pool, not volume usage (observability) — PostgreSQL disk supervision is yours, sized from your real source volumes. No measured growth multiplier exists yet (mirror ≈ the declared source tables plus system columns;rag.chunkadds text + a 768-float vector per chunk) — saying so beats a made-up number. Backups grow with the mirror; the script's--keep Nbounds how many dumps you retain.
Throughput & latency — the measured part
Load tests — sizing the hub with k6
Measure what a lumnik deployment can serve, find the knee of the curve, and
turn that into an infrastructure sizing decision. Scenarios are
k6 scripts (JavaScript); no local install needed — the runner
uses the grafana/k6 Docker image.
Quickstart (against a running self-host stack)
./loadtest/run.sh smoke.js # prove the harness: auth + routes, ~10 s
MAX_VUS=25 ./loadtest/run.sh read-mix.js # the sizing ramp, ~2.5 min
run.sh mints the Bearer token from the host (user integrator, password read
from the repo-root .env), so the token's issuer matches what the hub trusts.
Override with K6_USER / K6_PASSWORD / KC_URL / BASE_URL for another stack.
What is measured — and what deliberately is not
Measured: the hub's own read surface, weighted like real integrator usage —
/api/platform/version|connectors|runs, /api/views, /api/workflows,
/api/inbox, /api/entities. All list endpoints, all guaranteed 200 on any
stack, even empty. Every request is Bearer-authenticated and RLS-scoped, so the
measurement includes the real auth + tenant-isolation cost.
Not measured (v1, by design):
ask/ chat / RAG search — those calls are dominated by the model (Ollama or a remote provider), so a load test would size the model, not the hub. Size model capacity separately, on the model's own hardware.- Ingestion — a scheduled batch on virtual threads, not a concurrent user load; its cost is per-run, measured by run reports, not by VUs.
The sizing method
smoke.jsfirst — a failing smoke means the harness is wrong, not the hub. Run it twice: the first run also warms the JVM.- Ramp
read-mix.jsat increasingMAX_VUS(25 → 50 → 100 …) until either p95 bends sharply upward orhttp_req_failedleaves zero. The last clean step is the capacity of this deployment. - Watch the other half:
docker statson the hub and Postgres during the ramp. The knee is usually the database — connection pool saturation shows up as latency, not as errors. - Record one line per run in the sizing table:
| date | hardware (CPU/RAM) | stack | MAX_VUS | p95 worst endpoint | failed | verdict |
|---|---|---|---|---|---|---|
| 2026-08-17 | M1 Max / 64 GB | selfhost compose (image 2026-07-12, ONLY 5 surfaces¹) |
25 | runs: 210 ms | 0.00% | clean |
| 2026-08-17 | M1 Max / 64 GB | selfhost compose, current main, full 7-surface mix | 25 | connectors: 50 ms | 0.00% | clean — ~51 rps |
| 2026-08-17 | M1 Max / 64 GB | selfhost compose, current main, full 7-surface mix | 50 | runs: 50 ms | 0.00% | clean — ~101 rps, p95 flat vs 25 VUs, no knee in sight |
| 2026-08-18 | Hostinger KVM 4 VPS (4 vCPU EPYC / 16 GB / NVMe) | selfhost + TLS façade, via https² | 25 | runs: 10.6 ms | 0.00% | clean — ~56 rps |
| 2026-08-18 | Hostinger KVM 4 VPS (4 vCPU EPYC / 16 GB / NVMe) | selfhost + TLS façade, via https² | 50 | connectors: 10.9 ms | 0.00% | clean — ~112 rps, p95 flat vs 25 VUs, no knee |
¹ that image predated the workflows surface; after up.sh --build on current main
the full mix runs green. Note the 210 ms → 50 ms drop between the July image and
current main — the August performance campaign, visible in one table.
² measured through the full public path — Caddy, TLS, the façade issuer — with k6 running on the VM itself (so client network excluded, TLS stack included). A ~$15/mo VPS holds p95 ≈ 11 ms at 112 rps on the authenticated, RLS-scoped read mix: for a tester-scale or small-company deployment, the hub is not the bottleneck.
Honest caveats
- Localhost numbers size nothing but localhost. The sizing that counts is run on the target hardware, over the network the users will use.
- Access tokens from the bundled realm live 1 hour (
accessTokenLifespan: 3600) — ample for every scenario here. A soak test outliving the token needs a token-refresh loop (not built yet — build it when a soak test is actually needed), and a stricter IdP shortens the budget. - Thresholds (
P95_MS, default 500 ms) are regression gates, not SLOs. Tune them per environment; a threshold that cannot fail protects nothing. ONLY=name,namenarrows the mix explicitly — to isolate one surface, or against an older build that predates a surface. The default is always the full mix: a missing endpoint must go red, never silently vanish.
Files
| file | role |
|---|---|
run.sh |
token minting + dockerised k6 invocation |
k6/lib.js |
endpoints, weights, auth params |
k6/smoke.js |
1-VU harness check, CI-able |
k6/read-mix.js |
the ramping sizing scenario |
This page transcludes the real loadtest/README.md from the repository — like every
operational page on this site, it cannot drift from what actually runs. The scenarios
themselves live in loadtest/;
deploy first (self-host), then size.