Benchmark Results¶
This document is the single source of truth for the token-economy and
NFR performance numbers in the README and
index.md. Every number below is produced by an executable
test in tests/bench/token_economy_bench_test.go and re-validated by
make bench. No number here is hand-edited.
TL;DR — All headline claims from the README pass. Measured savings are 86-99% (depending on server shape), proxy overhead is ~12 µs/op (NFR1 wants <50 ms), and throughput is ~25,000 q/s in-process (NFR AC 16-3 wants ≥500 q/s). One previously-claimed number is corrected: the router payload is 158 tokens, not the older ~110 / 27.5 — see §3 Why the router number moved.
1. How to reproduce¶
# from repo root
make bench # runs the full suite, writes bench-results/<date>.txt
make bench-compare # diffs two result files (FILES=old.txt new.txt)
go test -run=^$ -bench=. -benchmem -benchtime=3s -count=3 ./tests/bench/...
The full suite runs in < 1 second end-to-end on a single core. It
does not require a live MCP server, network access, or a database —
every server-side path is exercised against the in-process
tests/bench/mockmcp library.
To refresh the canonical MCP server tool counts from a live run (e.g. after re-installing the GitHub / Garmin / Intervals.icu servers):
go run ./tests/bench/live_snapshot \
-config tests/bench/fixtures/live-snapshot.yaml \
-out tests/bench/fixtures/live-snapshot.json
Until that snapshot is refreshed, tests/bench/fixtures/live-snapshot.json
contains seeded counts derived from the previous docs/index.md numbers
(GitHub 41, Garmin 100, Intervals.icu 10 — Stitch is no longer available).
2. Methodology¶
2.1 Token counting¶
All token accounting in the benchmark suite uses the same primitive the runtime cost tracker uses:
import "github.com/mmornati/leanproxy-mcp/pkg/reporter"
estimator := reporter.NewEstimator() // 1 token ≈ 4 chars (chars/4)
tokens := estimator.EstimateTokens(payload) // or EstimateJSON(v)
This is pkg/reporter.Estimator, exposed as a public type in v0.9.0
specifically so the runtime and benchmarks can never disagree. The
chars/4 heuristic is a well-known approximation of BPE-style
tokenizers (OpenAI, Anthropic) for English text; it is not byte-perfect
but is consistent within itself, which is what matters for the
savings ratios reported here.
2.2 Native MCP baseline¶
For each MCP server in the live snapshot, the benchmark synthesises a
tools/list JSON payload whose byte size matches the per-server
schema_bytes field. The token count of that payload is the "Native MCP"
column. We use the raw token count (not the 0.25× cache-read cost)
because the README's "Schema Tax" claim is about the on-wire payload
size, independent of provider-side caching discounts.
The cache-read comparison in index.md is provided as a separate
column at 0.25× to mirror the original table.
2.3 LeanProxy router payload¶
The router is a 3-tool definition exposed by pkg/gateway/tools.go:
list_servers— list MCP servers configuredinvoke_tool— invoke a tool on a specific serverlist_tools— list tools on a specific server
The benchmark marshals this into the same {"jsonrpc":"2.0","id":1,
"result":{"tools":[...]}} envelope the production proxy returns, so
the 158-token figure includes the JSON-RPC envelope (id, jsonrpc
version, result wrapper).
2.4 Session replays¶
The three session tables (Morning Sport / Dev / Full Day) replay the
exact prompt sequences from the previous docs/index.md against the
synthesised per-server tool counts:
- Morning Sport — 2 servers, 4 prompts (Garmin + Intervals.icu)
- Dev Workflow — 2 servers, 5 prompts (GitHub + Intervals.icu)
- Full Day — 3 servers, 7 prompts (all available)
For each prompt, the "Native MCP" cost is the sum of the per-server
schema tax at 0.25× cache-read cost (matching the "real" cost model
in index.md). The "LeanProxy" cost is the router payload + one stub
schema (~26 tokens) per tool actually invoked.
2.5 NFR benchmarks¶
- NFR1 (proxy overhead) — microbenchmark of the JSON-RPC parse +
cost-track hot path. Includes one Unmarshal of the request, one
Unmarshal of the response, and one
TrackAtcall. - NFR2 (50 MB payload) — single-call
EstimateTokenson a 50 MB byte buffer. This is the worst-case size a single request can hit. - AC 16-3 (throughput) — in-process
mockmcp.Serverdriven in a tight loop. The number is the mockmcp ceiling, not the leanproxy binary ceiling; for a real binary-level measurement, use the in-tree e2e suite (tests/e2e/) which currently exercises ~5,000 req/s against a Python mock upstream. - NFR3 (binary size) —
os.Statover thedist/leanproxy-mcp-*binaries produced bymake build.
3. Why the router number moved¶
The README's previous "~110 router tokens" / "27 tokens" came from a
hand-counted estimate of the 3-tool schema (without the JSON-RPC
envelope) using a different token-counting rule (1 token per tool
field, then summed). The benchmark measures the full tools/list
response as it would appear on the wire using the runtime Estimator,
which is the right unit for the cost-saving claim.
For comparison:
| Measurement | Tokens | Source |
|---|---|---|
| Hand-counted 3-tool field sum (old) | ~27 | previous docs/index.md |
| Hand-counted 3-tool schema (old) | ~110 | previous README |
Full tools/list envelope (current) |
158 | tests/bench + Estimator |
| Per-stub on-demand schema (current) | 26 | tests/bench + registry.ToolStub |
4. Raw results (latest run, v0.9.0)¶
goos: darwin
goarch: arm64
pkg: github.com/mmornati/leanproxy-mcp/tests/bench
cpu: Apple M4
4.1 Schema-tax (per server)¶
| Server | Tools | Native tokens | Router tokens | Savings |
|---|---|---|---|---|
| Garmin | 100 | 11,134 | 158 | 98.6% |
| GitHub | 41 | 4,570 | 158 | 96.5% |
| Intervals.icu | 10 | 1,129 | 158 | 86.0% |
| All 3 | 151 | 16,833 | 158 | 99.1% |
4.2 Session replays (0.25× cache-read model)¶
| Session | Prompts | Native tokens | Lean tokens | Savings |
|---|---|---|---|---|
| Morning Sport | 4 | 12,260 | 740 | 94.0% |
| Dev Workflow | 5 | 7,120 | 925 | 87.0% |
| Full Day | 7 | 29,449 | 1,295 | 95.6% |
4.3 NFRs¶
| Benchmark | Measured | Threshold | Pass |
|---|---|---|---|
BenchmarkProxyOverhead_NFR1 (p50) |
~12 µs/op | <50 ms | ✅ |
BenchmarkLargePayload_NFR2 (50 MB) |
~7 ms | <200 ms | ✅ |
BenchmarkThroughput_MockMCP (in-process) |
~25,000 q/s | ≥500 q/s | ✅ |
TestBinarySize_NFR3 (darwin-arm64) |
15.8 MB | <20 MB | ✅ |
4.4 Per-primitive microbenchmarks¶
| Primitive | Time | Allocs |
|---|---|---|
BenchmarkEstimateTokens |
~50 ns | 0 |
BenchmarkEstimateJSON |
~250 ns | 1 |
5. What was corrected vs the previous README¶
| Old claim | Source | New claim | Notes |
|---|---|---|---|
| "90%+" headline | README | "86-99%" | Per-server variation is real |
| "~110 router tokens" | README, architecture | 158 tokens | Now includes the JSON-RPC envelope (full tools/list) |
| "27.5 LeanProxy tokens" | index.md | 158 tokens | Same correction; old number was a hand-counted schema-field sum, not the on-wire payload |
| "~54 tokens per stub" | configuration.md, architecture | ~26 tokens per stub | Stub is {name, description, category?} — measured from the production registry.ToolStub |
| "11 µs overhead at 5,000 RPS" | architecture.md | ~12 µs/op (p50) | Same order of magnitude; quoted from Bifrost originally — now our own number |
| "6-7× token reduction" | configuration.md, architecture | 86-99% reduction | Per-server ratio varies; use the new tables |
| 4-server column | README, index.md | 3-server (Stitch removed) | Stitch MCP is no longer available |
| Garmin 55 / Intervals 67 (README) | README | Garmin 100 / Intervals 10 | Resolved against docs/index.md; now consistent across both docs |
6. Future work¶
- Refresh
live-snapshot.jsonwith a real run ofgo run ./tests/bench/live_snapshotonce the Garmin / Intervals / GitHub credentials are wired into CI. The seeded numbers infixtures/live-snapshot.jsonare a placeholder. - Binary-level throughput — the in-process mockmcp number is a
ceiling, not the binary-level throughput. A
make bench-e2etarget that spawns the leanproxy binary + a mockmcp subprocess and measures end-to-end q/s is the next step (seebuildMockMCPintoken_economy_bench_test.go). - Real-tokenizer comparison — swap the
chars/4heuristic fortiktoken-goto get a tokenizer-accurate number. The Estimator API already supports this viaNewEstimatorWithRatio.
7. Commit / run metadata¶
| Field | Value |
|---|---|
| Version | v0.9.0 |
| Git | 7db8011 (Merge pull request #269) |
| Go | 1.25+ |
| Host | Apple M4, darwin/arm64 |
| Date | 2026-08-12 |