Liking cljdoc? Tell your friends :D

toolnexus performance benchmarks

Every number on this page is [MEASURED] on the machine described below, with a reproducible harness committed under ../benchmarks/. We publish only figures we actually measured. Where a framework could not be stood up to do the same work honestly, we say so and omit it from the table rather than guess. A partial-but-real table beats a complete-but-fake one.

This is the latency/throughput companion to comparison-benchmarks.md — that page compares features and spec-compliance and deliberately publishes no performance numbers; this page supplies the controlled head-to-head it points at. (We do not edit that file.)

Versions measured: toolnexus 0.13.0 · LangGraph 1.2.10 (+ langchain-mcp-adapters 0.3.1, langchain-openai 1.4.1) · Google ADK 2.6.1 (+ litellm 1.95.0) · CrewAI 1.15.10 · Pydantic AI 2.22.0 · OpenAI Agents SDK 0.19.2 · Vercel AI SDK 7.0.48 · LangChain.js 1.5.4 · Mastra 1.55.0 · Eino 0.9.12 · langchaingo 0.1.14 · LangChain4j 1.0.1 · Spring AI 1.0.1 (Spring Boot 3.4.2) · Semantic Kernel 1.65.0 · Microsoft.Extensions.AI 9.9.1 · brainlid/langchain 0.9.2 · MCP Python SDK 1.29.0 · mark3labs/mcp-go 0.48.0 · MCP Java SDK 2.0.0 · ModelContextProtocol (.NET) 1.4.0 · koine 0.11.0 · cljgo 0.9.0. Measured 2 August 2026, on the machine described under Hardware / OS — a different machine and toolchain generation from the July 2026 run this page previously carried. Frameworks move fast — re-run before quoting.


TL;DR

Pointing every framework at the same mock LLM and the same stdio MCP server, and measuring only the framework's own per-request cost for one fixed tool-calling scenario, across 39 framework configurations in 7 languages:

  • toolnexus has the lowest per-request overhead in Python, JavaScript, Go and Java, and is within noise of the fastest in C# and Elixir.
  • Python: toolnexus 1.00 ms p50/request vs OpenAI Agents 4.08 ms, Pydantic AI 4.95 ms, LangGraph 5.51 ms (persistent MCP session) / 23.2 ms (default reconnect-per-call), Google ADK 5.90 ms, CrewAI 9.27 ms4–23× less framework overhead, at half the memory and a fraction of the install size.
  • Go: the July stdio-MCP bug is fixed, so Go finally publishes a real MCP row: toolnexus 0.49 ms p50 over live MCP vs Eino 0.55 ms, in a 10 MB static binary and 17 MB RSS — the lowest overhead and the smallest footprint on the page.
  • Java: toolnexus 2.19 ms p50 vs LangChain4j 2.15 ms (a dead heat, LangChain4j fractionally ahead) and Spring AI 3.25 ms — with 2.6× less peak memory than Spring AI and a 279 KB jar + 8.6 MB deps vs a 26 MB Spring Boot fat jar.
  • Clojure is new to this page, and it is the slowest port here. Both hosts land at ~5.5 ms p50 over MCP — roughly 5× the Python port and 11× the Go port. That is not a rounding artefact and it is not hidden; it is analysed below.
  • Every competitor that ships an MCP client did real MCP here; toolnexus does real MCP in all seven languages.

These are framework-overhead numbers, not end-to-end latency: with a real model the 100 ms–second model call dominates. This benchmark isolates the tax the framework itself adds on top.


Methodology

Why a mock LLM (and not a real one)

The thing we want to compare is framework overhead — how much CPU, memory, and wall time a framework spends building its toolkit, serialising tool schemas, running its agent loop, dispatching tool calls, and parsing responses. A real model call costs 100 ms to several seconds and is provider-bound; it would swamp the framework's own cost and make the comparison about the model, not the framework. It would also cost money and be non-deterministic.

So we stand up a tiny local OpenAI-compatible server (mock_llm.py) that returns a scripted, deterministic conversation:

  1. Turn 1 (no tool results in the transcript yet) → an assistant message with two parallel tool_calls: get_weather(city="Paris") and add(a=2, b=2).
  2. Turn 2 (tool results present) → a final assistant message, "The weather in Paris is Sunny, 22C. And 2 + 2 = 4."

Every framework points its base_url at this server. To keep the work identical, the mock picks the tool names from the tool list each framework advertises — so Spring AI's prefixed spring_ai_mcp_client_bench_get_weather and toolnexus's bare get_weather are both handled, and each framework runs its full real tool-calling loop.

Fixed, shared tools

One stdio MCP server (mcp_server.py) exposes three tools (get_weather, add, echo). It's a raw JSON-RPC/stdio implementation (stdlib only) that echoes back the client's requested protocol version, so every MCP SDK in the comparison (Python mcp, mark3labs/mcp-go, the official Java SDK, Spring AI's client) connects to the same binary and pays the same tool-discovery cost. It's launched with one fixed Python interpreter for all frameworks.

toolnexus additionally supports agent skills and native tools; where relevant we report a full config (MCP + 1 skill + 1 native tool) alongside the strict MCP-only config, to show the extra sources don't change the overhead story.

The scenario

A single fixed question — "What's the weather in Paris and what is 2+2?" — forcing exactly one tool-calling turn. One scenario run = 2 LLM round-trips + 2 tool executions through the framework's own loop.

Metrics and how each was measured

MetricHowTool
Cold initWall time to build the toolkit/agent including MCP connect + tool discovery, first touch. Measured in-process on every port, so it excludes interpreter/JVM boot everywhere and stays comparable.time.perf_counter (Py), performance.now (JS), time.Now (Go), System.nanoTime (Java), Stopwatch (C#), monotonic_time (Elixir), koine.time/mono-ms (Clojure).
Per-request p50/p95/meanWall time of one scenario run, over N = 30 measured runs after 5 warmup runs. Clojure is the one exception — a sample there is a batch of 10 runs divided by 10, because koine's portable clock is millisecond-resolution; see the Clojure section.same honest timers, in-process.
Peak RSSMax resident set of the runner process./usr/bin/time -l wrapping each runner (macOS reports bytes).
Install size / depsIsolated per-framework footprint.du -sh of each venv's site-packages / jar / binary; declared direct deps.

Warmup handling. JVM figures (toolnexus-Java, Spring AI) are post-warmup — the first calls include JIT compilation and class loading and are excluded. Python and Go also warm up but far less. Treat cold-start-sensitive workloads (serverless) with the JVM caveat in mind.

Hardware / OS

MachineApple Mac17,8 — M5 Pro (arm64), 18 cores (6 efficiency + 12 performance), 48 GB RAM
OSmacOS 26.4 (Darwin 25.4.0)
Date2 August 2026
RuntimesCPython 3.11.15 (isolated uv venvs) · Node 24.18.0 · Go 1.26.3 · Temurin JDK 21.0.11 (Gradle-provisioned toolchain; Gradle 9.5.1 on OpenJDK 26) · .NET SDK 10.0.301 · Elixir 1.20.2 / OTP 29 · Clojure 1.12.5 on OpenJDK 26, and cljgo 0.9.0 (Go 1.26.3)
Networknone — mock LLM + MCP server are both local, offline, deterministic

Single machine, otherwise-idle, every row from one sitting on one day — which is the point of re-running the whole suite rather than splicing in a new language. The Clojure rows were sampled three independent times; p50 moved by at most 0.4 ms (JVM 5.1–5.5 ms, cljgo 5.6–5.8 ms), so treat ±0.5 ms as this harness's noise floor and do not read differences smaller than that as signal.

Different day, different toolchain generation. The hardware profile matches the July 2026 run (same Mac17,8 / 48 GB spec), but every runtime under it moved: Go 1.23 → 1.26.3, Node 22 → 24.18, the .NET SDK and Elixir/OTP forward a generation, and every competitor library re-resolved to its current release. Absolute numbers shift for that reason alone — compare ratios within this table, not cells against the July one.


Results

All cells [MEASURED], N = 30 runs, 5 warmup, single machine (above). ok = the loop produced the correct final answer end-to-end. Sorted by language, then p50.

Rows marked (native) use in-process tools with the same names and behavior instead of the shared MCP server. Native skips the subprocess spawn and tools/list, so a native row has an inherent small advantage over an MCP row — the two are kept explicitly labelled rather than mixed.

FrameworkLangTool sourceInit (ms)p50 (ms)p95 (ms)mean (ms)Peak RSS (MB)Toolsok
toolnexusClojure (JVM)native01.73.11.89511⁴2
toolnexusClojure (cljgo)native01.71.91.7429.62
toolnexusClojure (JVM)MCP245.56.85.55510⁴3
toolnexus (+ skill + native)Clojure (JVM)MCP255.56.85.46492⁴5
toolnexusClojure (cljgo)MCP185.75.95.6931.33
toolnexus (+ skill + native)Clojure (cljgo)MCP186.26.56.1930.85
Semantic KernelC#native510.5940.7920.62171.22
toolnexusC#MCP840.6260.8160.63778.13
Microsoft.Extensions.AIC#native270.6260.7200.61668.22
toolnexus (+ skill + native)C#MCP960.6570.8590.68380.85
toolnexusElixirMCP270.6020.6640.6051233
toolnexusElixirnative80.6020.7030.5941162
Elixir LangChainElixirnative00.6460.7490.6591112
toolnexusGonative00.4180.4710.42314.92
toolnexus (+ skill + native)GoMCP130.4880.6480.50617.55
toolnexusGoMCP¹120.4910.6760.51217.03
EinoGoMCP140.5450.6280.54827.63
langchaingoGonative00.5481.0460.61021.62
LangChain4jJavaMCP2112.1453.0722.2771253
toolnexusJavaMCP3892.1873.3462.3311553
toolnexus (+ skill + native)JavaMCP3122.1912.8572.2271715
Spring AIJavaMCP883²3.2457.4663.9564003
toolnexusJSnative00.760.890.771322
toolnexusJSMCP160.911.090.911343
toolnexus (+ skill + native)JSMCP190.931.150.941355
Vercel AI SDKJSnative01.101.511.141392
Vercel AI SDKJSMCP161.201.521.201393
LangChain.jsJSnative22.262.952.322492
LangChain.jsJSMCP202.423.432.462493
MastraJSMCP182.593.772.692853
MastraJSnative02.613.612.692782
toolnexus (+ skill + native)PythonMCP170.9761.1380.99658.85
toolnexusPythonMCP140.9981.1001.00458.83
OpenAI Agents SDKPythonMCP194.0774.3164.0681103
Pydantic AIPythonMCP274.9545.2934.9881393
LangGraph (persistent session)PythonMCP1155.5085.9315.5041273
Google ADKPythonMCP175.8966.7306.0052903
CrewAIPythonMCP379.26610.0979.4292133
LangGraph (reconnect-per-call)³PythonMCP12423.2424.2723.411273

¹ The July stdio-MCP bug is fixed. Last time this page had to publish a native-only Go row because the port killed its own MCP child on connect (the bug, kept for the record). Go now discovers over a live stdio session like everyone else, and its MCP row costs ~0.07 ms more than its native row — the honest price of the subprocess. ² Spring AI "init" is Spring Boot context startup incl. MCP discovery, not a toolkit-only build — see caveats; not directly comparable to toolnexus's init. ³ LangGraph's documented default (MultiServerMCPClient.get_tools()) re-opens a fresh stdio session — re-spawning the MCP subprocess — on every tool call. The session row holds one persistent MCP session (apples-to-apples with toolnexus). ⁴ The Clojure-on-JVM RSS numbers are ~500 MB and that is not a typo. They are the JVM's default heap sizing on a 48 GB machine (clojure -M sets no -Xmx), not memory the port needs — the Java port on the same machine sits at 155 MB because its launcher script is more conservative. It is peak resident set, so it is a real number and it is published as measured; it is a JVM configuration figure, not a toolnexus one. The same source compiled by cljgo runs the identical workload in 30 MB.

Install footprint / dependencies [MEASURED]

FrameworkInstall sizeDirect deps
toolnexus (Python)34 MB venv2 — mcp, pyyaml (the client loop itself is pure stdlib)
OpenAI Agents SDK (Python)65 MB venvopenai-agents, mcp
LangGraph (Python)87 MB venv3 — langgraph, langchain-mcp-adapters, langchain-openai
Pydantic AI (Python)130 MB venvpydantic-ai, mcp
Google ADK (Python)230 MB venv2 — google-adk, litellm
CrewAI (Python)787 MB venvcrewai, crewai-tools[mcp]
toolnexus (JS)26 MB@modelcontextprotocol/sdk
Vercel AI SDK (JS)23 MBai, @ai-sdk/openai, @ai-sdk/mcp, zod
LangChain.js (JS)106 MBlangchain, @langchain/openai, @langchain/mcp-adapters
Mastra (JS)144 MB@mastra/core, @mastra/mcp, @ai-sdk/openai, zod
toolnexus (Go)10 MB static binarysingle binary, no runtime
langchaingo (Go)16 MB binarytmc/langchaingo
Eino (Go)21 MB binarycloudwego/eino + eino-ext
toolnexus (Java)279 KB jar + 8.6 MB depsMCP SDK, Jackson, SnakeYAML
LangChain4j (Java)11 MB distlangchain4j, -open-ai, -mcp
Spring AI (Java)26 MB Spring Boot fat jarSpring Boot + Spring AI OpenAI + MCP-client starter
toolnexus (C#)3.2 MBModelContextProtocol, YamlDotNet
Microsoft.Extensions.AI (C#)5.4 MBMicrosoft.Extensions.AI[.OpenAI]
Semantic Kernel (C#)7.8 MBMicrosoft.SemanticKernel
toolnexus (Elixir)5.3 MB depsin-house MCP client
Elixir LangChain (Elixir)5.3 MB depslangchain (brainlid)
toolnexus (Clojure, cljgo)15 MB static binary1 — koine (net.clojars.muthuishere/koine, a 68 KB jar), nothing else
toolnexus (Clojure, JVM)the port + a 68 KB koine jar, on the Clojure runtimesame single dependency; the port's own deps-purity gate fails the build if anything else appears on the default classpath

Reproduce it

Full instructions, for all 39 configurations: ../benchmarks/README.md — it lists every venv, every build command, and the env var that switches each runner on. The shape is always the same:

export REPO=/path/to/toolnexus VENVS=/tmp/bench-venvs

# 1. install each framework in its own isolated venv / node_modules / module cache
#    (toolnexus always from the LOCAL repo — editable install, `replace`, composite
#     build, ProjectReference, path dep, or a symlinked source tree)
# 2. build the compiled runners (Go, JVM, .NET, and the cljgo AOT binary)
# 3. start the mock LLM
"$VENVS/toolnexus/bin/python" "$REPO/benchmarks/mock_llm.py" --port 8900 &
# 4. export one env var per framework you want measured, then:
python3 "$REPO/benchmarks/run_all.py"

Three traps worth naming, all hit while producing this page:

  • Pin mcp<2 in the LangGraph and Google ADK venvs. mcp 2.0.0 dropped mcp.shared.session and mcp.shared.context.RequestContext, which breaks langchain-mcp-adapters and ADK's mcp_toolset at import time.
  • CrewAI needs crewai-tools[mcp]. Without the extra, MCPServerAdapter hits a click.confirm prompt and aborts on a non-tty. Keep BENCH_WARMUP ≥ 2 for CrewAI, too: its first call writes a tracing preference and shows up as a ~480 ms outlier.
  • For Clojure, build the cljgo leg with the published cljgo binary ($HOME/go/bin/cljgo), not a PATH shim that rebuilds the compiler from a local checkout — otherwise you are measuring a working tree, not a release.

Couldn't measure / caveats — read this before quoting

Clojure: the slowest port on this page, and why

The Clojure port is ~5.5 ms p50 over MCP on both hosts. The Python port does the same work in 1.0 ms and the Go port in 0.49 ms. Clojure is last, by a factor of five, and the number is published rather than framed away. Three things are worth separating before anyone quotes it:

1. It is not the loop — it is the MCP round trip. With native in-process tools the Clojure port runs the whole scenario in 1.7 ms on both hosts, comfortably mid-table. Swapping those two tools for the same two tools over the shared stdio MCP server adds ~3.9 ms, i.e. roughly 2 ms per tools/call. Every other port pays a fraction of that for the identical wire traffic (Go: +0.07 ms; C#: within noise; Python: ~0). So the cost sits in this port's stdio JSON-RPC path — its blocking line-read round trip and pure-Clojure JSON codec — not in the agent loop, the adapters or the toolkit. That is a concrete optimisation target, and it is the single biggest one on this page.

2. The two hosts agree, which is the point of the port. JVM 5.5 ms, cljgo 5.7 ms, from the same source filebenchmarks/run_toolnexus_clojure/src/bench.cljc is compiled by cljgo and loaded by clojure -M with no reader conditional between them. Parity is the product; the hosts differing by 0.2 ms is the evidence that it holds under load, not just in tests.

3. Where the hosts do not agree is memory and startup, and both favour cljgo.

Clojure (JVM)Clojure (cljgo)
p50 / request (MCP)5.5 ms5.7 ms
in-process cold init24 ms18 ms
process cold start (exec → first answer)~1.13 s~0.02 s
Peak RSS~500 MB (JVM default heap)~31 MB

The JVM's ~1.1 s process cold start is real and is reported here rather than excused. init_ms in the results table is measured in-process on every port, so it excludes interpreter and JVM boot everywhere — that is what keeps the column comparable. But for Clojure-on-JVM that exclusion hides the dominant cost of a short-lived run: clojure -M -e nil alone is ~0.32 s on this machine, and the full runner to a first answer is ~1.13 s, against ~0.02 s for the cljgo binary (median of 3 each). For a long-lived agent process this is amortised to nothing. For a CLI, a lambda, or anything that starts per request, it is the whole story — and it is exactly the case cljgo exists for.

Measurement caveat, stated because it changes how you read p95. koine's portable monotonic clock has millisecond resolution on both hosts, and a sub-millisecond timer would mean host-specific code in the one file whose entire claim is that it has none. So a Clojure sample is a batch of 10 consecutive runs divided by 10 (0.1 ms effective resolution). Mean is unaffected; p50 and p95 are percentiles over batch means and are therefore smoother than every other port's per-run percentiles — a single slow request is averaged across its batch instead of standing alone in the tail. Read the Clojure p95 as "the 95th-percentile 10-run stretch". Three independent samples put p50 within 0.4 ms.

Go stdio-MCP: the bug we found, and fixed

For the July 2026 run the toolnexus Go port could not hold a live stdio MCP session, so that table published a native-tools-only Go row. That is fixed as of 0.9.2, and this run's toolnexus-go-mcp row is a real live stdio session (3 tools discovered, 0.491 ms p50). The original diagnosis is kept below because the failure mode — a silent, isolated MCP failure that looks like "no tools" — is worth recognising anywhere:

  • golang/mcp.go's newLocalClient starts the transport with a timeout context and defer cancel()s it before returning the connected client.
  • mark3labs/mcp-go spawns the MCP child with exec.CommandContext(ctx, …) (client/transport/stdio.go), so that context owns the subprocess lifetime.
  • On return, defer cancel() fires → the MCP child gets SIGKILL immediately after initialize succeeds → the subsequent tools/list hits a dead pipe (transport error: transport closed), tools load as 0, and (because MCP failures are isolated) it fails silently.

The Go port's test suite didn't exercise a live outbound stdio MCP client (its MCP tests were all inbound server tests), so it went unnoticed until a benchmark asked the port to do the thing nobody had asked it to do in anger. That is the argument for keeping this harness: it exercises each port the way a user does, not the way its unit tests do.

JVM warmup

Java, Spring AI, LangChain4j and Clojure-on-JVM numbers are post-warmup. Cold-JVM first requests are materially slower (JIT + class loading). For short-lived / serverless workloads the JVM cold-start dominates and none of these per-request numbers apply. The Init (ms) column captures some, but not all, of that cold cost — for the Clojure case the process-level figure is measured and published above.

Spring AI "init" is not a like-for-like init

Spring AI's MCP discovery is entangled with Spring Boot context startup, so its Init figure (~1.06 s) includes bringing up the whole application context, not just a toolkit build. It's reported for transparency but should not be compared cell-to-cell against toolnexus's toolkit-only init. The per-request figure is the clean, directly-comparable one.

Harness detail: chunked request bodies

Spring AI's RestClient sends the chat request with Transfer-Encoding: chunked and no Content-Length; the mock had to decode chunked bodies for Spring to work at all. That's a property of the HTTP client, not a Spring cost, and doesn't affect the measured latency (the mock decode is microseconds).

Mock-vs-real, and single-machine variance

  • These are framework-overhead numbers. In production, a real model call dwarfs all of them; a 5 ms vs 1 ms framework difference is invisible next to a 500 ms model call per request, but compounds under high concurrency, in tight agent loops, and on memory/cost at scale.
  • Single machine, single process at a time, otherwise idle. Absolute numbers will differ on your hardware; the ratios between frameworks are the portable takeaway. The Clojure rows were sampled three times and agreed within 0.4 ms.
  • All seven ports are measured this time, each against the competitors that exist in its language. Where a language has no MCP-capable competitor (C#, Elixir, Clojure), the table says so rather than inventing a peer: semantic-kernel and ms-extensions-ai are compared on native tools, langchain-elixir likewise, and Clojure has no third-party peer at all.
  • Nothing was skipped in this run. Every registered runner produced a result; the skipped list in benchmarks/results.json is empty. If a future run cannot stand a framework up, it lands in that list rather than quietly vanishing from the table.

Verdict

On pure framework overhead for a fixed MCP tool-calling scenario, toolnexus is the lowest-overhead option in Python, JavaScript, Go and Java, and within noise of the fastest in C# and Elixir — while doing real MCP in all seven languages and carrying the smallest or near-smallest install footprint everywhere. Python is 4–23× lighter than its competitors; Go now publishes a genuine MCP row at 0.49 ms in a 10 MB binary; Java is a dead heat with LangChain4j and 1.5× lighter than Spring AI at 2.6× less memory.

The honest asterisks, all of them: LangChain4j edges the Java port by 0.04 ms; Semantic Kernel edges the C# port by 0.03 ms on native tools while toolnexus is doing live MCP; and the Clojure port is last on this page by a factor of five, with ~2 ms of that per MCP tool call sitting in its stdio JSON-RPC path — a specific, findable cost, now written down where it cannot be forgotten. Clojure-on-JVM also carries a ~1.1 s process cold start and a ~500 MB default JVM heap; the same source compiled by cljgo starts in 20 ms and holds 31 MB. These figures describe warm, long-lived agents — which is exactly where framework overhead compounds and matters most.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close