soul.demarkus.io:6309/research/kind-bench-merged-binary.md/v1 draft reader meta

kind bench of the merged knowledge binary

First load and memory runs of demarkus-knowledge 0.51.0 (knowledge server, broker and both MCP gateways in one process) on a local kind cluster, 2026-10-01. Audience: demarkus maintainers. Scope is an early read, not capacity numbers for GKE; the plan is Merged binary load test, the pre-merge production picture is the GKE memory baseline.

Setup

  • Cluster demarkus-bench from deploy/kind/up.sh --with-knowledge --with-mcp-smoke with the released image ghcr.io/latebit-io/demarkus-knowledge:0.51.0 (32 smoke checks passed), then tools/benchmarks/kind/values-bench.yaml: 3 replicas, 25m CPU and 256Mi requested, 1 CPU and 1Gi limit as in production, GODEBUG=gctrace=1, and a bootstrapped publish token.
  • One world, world-default, in fake-gcs-server 1.52.2 (default filesystem backend). Mock OIDC for logins. Rate limits off.
  • Host: Apple silicon, Docker VM with 8 CPUs and 12 GiB, one kind node shared by the load generator, fake-gcs-server and the three pods.
  • Load from tools/demarkus-loadtest in an in-cluster pod; memory from the pods' cgroups every 5 s (working set as kubelet computes it); live heap from gctrace. Harness and raw output on branch bench/merged-binary-kind (tools/benchmarks/kind/).

Reads

Step Workers req/s p50 p99
QUIC FETCH, pooled connections 1 53 20 ms 26 ms
4 160 21 ms 45 ms
16 159 100 ms 166 ms
64 158 423 ms 486 ms
256 151 1.77 s 1.79 s
QUIC FETCH, fresh connection per request 16 157 23 ms 323 ms
64 159 343 ms 1.16 s
QUIC LOOKUP 16 159 101 ms 124 ms
64 158 501 ms 506 ms
QUIC LIST 16 159 80 ms 185 ms
MCP mark_fetch 1 53 20 ms 26 ms
8 160 60 ms 85 ms
32 160 260 ms 268 ms
MCP mark_lookup_all (one world) 1 53 20 ms 27 ms
8 160 40 ms 104 ms
32 160 240 ms 287 ms

Zero errors in about 65,000 requests. The three pods together used 0.12 to 0.37 cores and 65 to 118 MiB; no pod passed 51 MiB.

  • The ceiling is fake-gcs-server, not the server. Throughput flattens at about 160 req/s from 4 workers on while the pods idle at a fraction of a core; more workers only queue.
  • Every read costs two bucket round trips. OpenReadView stats the world's head object (refreshSnapshot) before each read, then reads the object, so even a LOOKUP answered from the in-memory catalog waits on the bucket. That is the 20 ms floor here and sets the floor on GCS too. The server's own read capacity needs the GKE scratch pool.
  • Handshakes are cheap. A fresh connection per request raised CPU from 0.23 to 0.30 cores at the same rate, about 0.5 ms of server CPU per QUIC handshake.
  • The MCP gateway adds nothing visible over direct QUIC: identical latency at one worker.
  • Connections are cheap in memory: 256 open connections took the three pods from about 65 to 117 MiB in total.

Writes

  • Commit pacing caps a world at about 0.67 writes/s. bucketstore waits defaultCommitInterval (1.5 s) between head-object commits, to stay under GCS's per-object update limit. One sequential writer measured exactly 1.51 s per publish when back to back; 4 concurrent writers queued to 6 s each; a 4-writer burst of 200 publishes lost 49 to the 10 s request timeout. Spaced below the pacing, a write takes about 60 ms.
  • Soak, 30 minutes: one writer at 0.5 writes/s (about 18 times production's agent) over 20 paths with retention 20, so every write after minute 13 pruned. 899 writes, 0 errors, p50 58 ms, p99 86 ms. The writer pod's working set stayed between 25 and 54 MiB; live heap went from 7 MB to 9 MB by minute 15 and stayed there. The other two pods held 24 to 36 MiB. The writer memory fix holds under write load; before it, kind grew 25 to 357 MiB over 630 writes.
  • All writes landed on one pod, as in production.

MCP session churn

5 new sessions a second for 40 minutes, each running initialize, initialized and one mark_fetch and never sending DELETE: 11,999 sessions, 0 errors, p50 10 ms for the three calls. Kube-proxy balances per TCP connection and the client reuses connections, so every session landed on one pod.

Time Live heap Working set Sweeps
start 7 MB 56 MiB 4
+10 min 42 MB 103 MiB
+20 min 72 MB 141 MiB
+30 min 96 MB 199 MiB 47
14:36 sweep, under load 112 then 99 MB 230 to 258 MiB 1,953
load ended +40 min 104 MB 237 MiB
14:51 sweep, idle 107 then 64 MB 195 then 156 MiB 6,453
  • About 9.6 KB of live heap per session, matching the earlier harness on mcp-go.
  • The idle TTL bounds it. Each sweep removed exactly the sessions it should (1,906 then 4,500), and live heap fell with them.
  • Sessions live 30 to 45 minutes, not 30. mcp-go sweeps every TTL/2, so steady-state live sessions are the new-session rate times 30 to 45 minutes. At 5 a second that is up to 13,500 sessions and about 130 MB of live heap on one pod; working set peaked at 263 MiB. Production's 40 to 99 sessions an hour is under 1 MB. One subject at the production limit of 120 calls a minute can hold about 2,700 sessions (about 27 MB); many subjects multiply that, and 1 GiB is reached around 15 to 20 new sessions a second sustained.
  • Log spam past 4,096 live sessions. The fetch dedup store caps at 4,096 sessions and logs a WARN on every eviction: 5,307 lines in 20 minutes, one per new session. It only triggers above about 1.5 new sessions a second, but under a burst it is one line per session.
  • kubelet rotated the pod log every 10 MB (about 30 minutes at this rate, mostly the per-request INFO line).

Findings to act on

  1. Read latency is bound by two bucket round trips per request; measure on GKE with real GCS before choosing whether a short head-stat cache is worth its staleness.
  2. A world takes at most about 0.67 writes/s; concurrent writers to one world queue and time out past six or so. Fine for the agent, worth knowing for bursty multi-agent writes.
  3. Session memory is bounded by the TTL but scales with the session rate, with a 45 minute worst case; a shorter TTL or a session cap would tighten it.
  4. Rate-limit the dedup cap WARN (log once per interval with a count).

Reproduce

tools/benchmarks/kind/README.md on branch bench/merged-binary-kind: cluster, overlay, load generator pod, seed, scenarios.sh reads tools writes churn, summarize.py.

trail
  1. soul.demarkus.io:6309 v1