|
| 1 | +# Cortex Load Test Report |
| 2 | + |
| 3 | +## Production results — Cloud Run (publishable) |
| 4 | + |
| 5 | +- Date: 2026-07-13 |
| 6 | +- Environment: Google Cloud Run (`cortex`, us-central1), live production service and revision config |
| 7 | +- Client: k6 via Docker, single client in Europe → us-central1 (network RTT ~145 ms floor is included in all latencies) |
| 8 | +- Method: per-IP rate limiter (`RATE_LIMIT_DEFAULT`, default 60/min) temporarily raised via env var for the run, then restored and re-verified (60 pass / overflow 429s confirmed after revert) |
| 9 | + |
| 10 | +| Scenario | Load | Requests | Error rate | p50 | p95 | p99 | Max | |
| 11 | +|---|---|---|---|---|---|---|---| |
| 12 | +| API health (prod) | 20 req/s × 1m | 1,197 | 0.00% | 146 ms | 154 ms | 164 ms | 373 ms | |
| 13 | +| API health (prod) | 200 req/s × 1m | 12,000 | 0.00% | 146 ms | 155 ms | 166 ms | 3.15 s | |
| 14 | + |
| 15 | +### Publishable claims |
| 16 | + |
| 17 | +- **Production sustains 200 req/s with 0% errors and no latency degradation**: p95 held flat (154 ms → 155 ms) from 20 to 200 req/s — a 10× load increase. |
| 18 | +- **13,197 production requests, zero failures.** |
| 19 | +- Latency is **network-bound, not server-bound**: ~145 ms of every measurement is Europe→us-central1 RTT; server processing is single-digit ms (confirmed by the local runs below on identical code). |
| 20 | +- Cloud Run **autoscaling absorbed the 10× jump transparently**; the single 3.15 s outlier is one instance cold start, after which p99 stayed ≤166 ms. |
| 21 | +- **Per-IP rate limiting (60/min) enforced in production**, verified empirically: 70 parallel requests → exactly 60 accepted, remainder rejected with HTTP 429. |
| 22 | + |
| 23 | +Not claimed: a long soak. A 10-minute 200 req/s sustained-stability run was intentionally not executed against live production from this session; run it during a maintenance window if you want a "sustained for N minutes" claim. |
| 24 | + |
| 25 | +## Local results — bottleneck discovery |
| 26 | + |
| 27 | +- Date: 2026-07-13 |
| 28 | +- Environment: local-dev (macOS, uvicorn single worker, Redis + Neo4j via Docker) |
| 29 | +- Tool: k6 (`grafana/k6` Docker image), constant-arrival-rate executor |
| 30 | +- Backend flags: `RATE_LIMIT_DEFAULT=100000/minute` (raised for benchmark), `RAG_PERF_HEADERS=true`, `RAG_SUGGESTIONS_DEFERRED=true` |
| 31 | + |
| 32 | +> Local numbers are for bottleneck discovery and sizing, not public production-capacity claims (see `load-tests/README.md`). |
| 33 | +
|
| 34 | +## Headline results |
| 35 | + |
| 36 | +| Scenario | Load | Requests | Error rate | p50 | p95 | p99 | Max | |
| 37 | +|---|---|---|---|---|---|---|---| |
| 38 | +| API health | 20 req/s × 1m | 1,201 | 0.00% | 3.9 ms | 5.2 ms | 6.7 ms | 14.1 ms | |
| 39 | +| API health | 200 req/s × 1m | 12,000 | 0.00% | 1.9 ms | 3.2 ms | 4.1 ms | 12.7 ms | |
| 40 | +| Internal agent loop (real LLM) | 1 req/s × 30s | 29 | 0.00% | 1.21 s | 2.0 s | 2.6 s | 2.77 s | |
| 41 | + |
| 42 | +## Portfolio-ready claims |
| 43 | + |
| 44 | +- Sustains **200 req/s with 0% errors and p95 latency of 3.2 ms** on the API layer (single uvicorn worker, local). |
| 45 | +- **Zero failed requests across 13,230 total requests** in all scenarios. |
| 46 | +- Real agent-loop turns (LLM call included, tools unbound) complete with **median 1.2 s / p95 2.0 s** at 1 req/s sustained. |
| 47 | +- Built-in **per-IP rate limiting (60/min default)** verified under load: at 20 req/s the limiter correctly rejected 90% of over-quota requests with HTTP 429 before it was raised for the benchmark — a resilience feature, confirmed empirically. |
| 48 | + |
| 49 | +## Observations |
| 50 | + |
| 51 | +- API latency *dropped* from 20 → 200 req/s (p95 5.2 → 3.2 ms): connection reuse across more concurrent VUs amortizes setup; the app layer is nowhere near saturation at 200 req/s. |
| 52 | +- Agent-loop latency is dominated by the LLM provider call (~1.2 s median), not the framework — per k6 bottleneck hints and `x-rag-perf` phase headers. |
| 53 | +- Peak k6 VUs: 50 preallocated, ≤3 used for the agent loop; 1 dropped iteration at 1 req/s (VU briefly busy on a 2.7 s turn). |
| 54 | + |
| 55 | +## Per-scenario reports |
| 56 | + |
| 57 | +- [prod-health-20rps-report.md](prod-health-20rps-report.md) — production, 20 req/s |
| 58 | +- [prod-health-200rps-report.md](prod-health-200rps-report.md) — production, 10× load |
| 59 | +- [health-report.md](health-report.md) — local, 20 req/s baseline |
| 60 | +- [health-200rps-report.md](health-200rps-report.md) — local, 10× load |
| 61 | +- [internal-agent-loop-report.md](internal-agent-loop-report.md) — local, real `_run_agent_loop` over HTTP |
| 62 | + |
| 63 | +## Reproduce |
| 64 | + |
| 65 | +```bash |
| 66 | +# backend |
| 67 | +RATE_LIMIT_DEFAULT=100000/minute INTERNAL_DISPATCH_SECRET=bench-secret \ |
| 68 | +INNGEST_DEV=1 INNGEST_SIGNING_KEY=local-benchmark-signing-key \ |
| 69 | +uv run uvicorn src.api.endpoints:app --host 0.0.0.0 --port 8010 |
| 70 | + |
| 71 | +# health @ 200 rps |
| 72 | +docker run --rm -v "$PWD:/work" -w /work \ |
| 73 | + -e BENCHMARK_BASE_URL=http://host.docker.internal:8010 \ |
| 74 | + -e BENCHMARK_RATE=200 -e BENCHMARK_PREALLOCATED_VUS=50 -e BENCHMARK_MAX_VUS=200 \ |
| 75 | + grafana/k6 run --summary-export reports/benchmarks/health-200rps-summary.json load-tests/health.js |
| 76 | + |
| 77 | +# agent loop (real LLM calls) |
| 78 | +docker run --rm -v "$PWD:/work" -w /work \ |
| 79 | + -e BENCHMARK_BASE_URL=http://host.docker.internal:8010 \ |
| 80 | + -e BENCHMARK_INTERNAL_SECRET=bench-secret -e BENCHMARK_BIND_TOOLS=false \ |
| 81 | + grafana/k6 run --summary-export reports/benchmarks/internal-agent-loop-summary.json load-tests/internal_agent_loop.js |
| 82 | + |
| 83 | +# render markdown |
| 84 | +uv run python scripts/render_k6_report.py --summary-json <summary.json> --scenario <name> \ |
| 85 | + --environment local-dev --target "<rate x duration>" --output <report.md> |
| 86 | +``` |
| 87 | + |
| 88 | +## Next steps for stronger claims |
| 89 | + |
| 90 | +1. ~~Re-run against the Cloud Run production deployment~~ — done, see production section above. |
| 91 | +2. Authenticated `agent_chat.js` run (needs `BENCHMARK_BEARER_TOKEN` + `BENCHMARK_AGENT_ID`) to quantify the full RAG chat path under load. |
| 92 | +3. 10-minute soak at 200 req/s against production (run during a maintenance window) to claim sustained stability, and a ramp test to find the actual saturation point. |
0 commit comments