What it costs¶
This page is the motivation for the library, stated in numbers rather than in
adjectives. It compares this package with the official Python SDK on one
tools/call, per protocol revision, for the server and for the client
separately, over HTTP and over a pipe.
The harness lives in benchmarks/ in the source tree and is not installed
with the package. Reproduce any row with:
uv run python -m benchmarks.run --suite server
uv run python -m benchmarks.run --suite client
uv run python -m benchmarks.run --suite stdio
uv run python -m benchmarks.run --concurrency 32 --calls 3000
uv run python -m benchmarks.schema_cost # one request, without a socket
How to read it¶
Both libraries serve the same tool – add two integers, return a model. What differs is the protocol machinery between the socket and that function.
The official SDK row is its current high-level server (MCPServer in
mcp==2.0.0; older SDK releases called the comparable API FastMCP). This is
an implementation comparison, not a claim that the two projects expose the
same public class names. The benchmark uses the SDK version pinned in the
conditions below.
Over HTTP they do not stand on the same web framework: this package is aiohttp, the official SDK is Starlette on uvicorn. So every HTTP table ends with two rows that speak no protocol at all – one hand-written handler per stack, parsing the request and answering a fixed JSON body. Compare a row with the floor of its own stack, not with the other library’s. Without those rows a reader compares two web servers and believes they compared two MCP libraries.
The stdio tables have no such problem, and are the cleanest comparison here: one subprocess, one pipe, no framework on either side.
The server rows have no client library in them. The bytes of one real request
are recorded, then replayed with nothing but aiohttp in the way. The client
rows hold the server constant and vary the client, including the cross
pairings: this package’s client against the SDK’s servers, and the SDK’s
client against this one.
Conditions¶
Apple M4, 10 cores, macOS 26.2. Python 3.14.2, aiohttp 3.14.3, pydantic
2.13.5, mcp 2.0.0, uvicorn 0.53.0, starlette 1.6.0. Loopback, no TLS.
One call at a time, --calls 1500 --warmup 200 --repeats 3; every row is
measured three times in rotation and keeps its best round.
Every logger is set to ERROR, on both sides. This matters: starting the
SDK’s server sets the root logger to INFO and installs a rich handler for
the whole process, after which its stateless server writes a line per request
and httpx writes another. Left on, that costs whoever talks to it 17-25% of
its rate. It is a real cost of a deployment that logs at INFO, and it is not
what these tables measure, so it is turned off for both alike.
Server over HTTP, per revision¶
Calls a second.
Server |
2024-11-05 |
2025-03-26 |
2025-06-18 |
2025-11-25 |
2026-07-28 |
|---|---|---|---|---|---|
aiohttp-tiny-mcp |
5722 |
5685 |
5839 |
5780 |
5458 |
official SDK, with session |
1263 |
1237 |
1277 |
1235 |
1616 |
official SDK, stateless |
1085 |
1105 |
1079 |
1095 |
1649 |
aiohttp, no protocol |
7208 |
||||
uvicorn, no protocol |
4866 |
The same tool returning a plain string instead of a model:
Server |
2024-11-05 |
2025-03-26 |
2025-06-18 |
2025-11-25 |
2026-07-28 |
|---|---|---|---|---|---|
aiohttp-tiny-mcp |
5621 |
5624 |
5680 |
5680 |
5428 |
official SDK, with session |
1272 |
1288 |
1278 |
1239 |
1909 |
official SDK, stateless |
1059 |
1086 |
1062 |
1060 |
1888 |
aiohttp, no protocol |
7193 |
||||
uvicorn, no protocol |
4809 |
The revision costs this package nothing measurable. Five revisions, and
the spread between them is smaller than the spread between two runs of the
same row. Serving 2024-11-05 and 2026-07-28 from one set of handlers is
not paid for at call time.
What the SDK spends is largely the session. Same server, same handler,
same stack; only the revision differs. The four revisions that carry a session
sit near 1100-1300 calls a second. 2026-07-28, the one with no session at
all, reaches 1600-1900.
The SDK’s stateless mode is the slower of its two. stateless_http=True
does not remove the session – it builds and destroys one per request – and
on the legacy revisions it costs about 13% against keeping one.
At 32 calls in flight the gap widens rather than closes: this package reaches 7673-8325 against a floor of 12654, while the SDK reaches 1402-1571 against a floor of 7698. The tails separate further than the rates do: 4.8-6.4 ms at the 99th percentile here against 31-92 ms there.
Listing the catalogue behaves like calling it: tools/list runs at 5420-5637
against the same floors, or 76-80% of them. Revisions before 2026-07-28
simplify a schema for the client, and that projection is derived once per
revision and kept on the tool, so a catalogue is not rebuilt per request.
benchmarks/schema_cost.py measures the same request with no transport under
it, which is where that shows. On 2025-11-25, with a twelve-field argument
model carrying $defs, enums and nullable unions, one tools/call costs 19
us and one tools/list 31 us for a 3 KB answer. The two-field tool of the
tables above costs 18 us and 14 us. A larger schema therefore moves
tools/list, which writes it, and leaves tools/call, which does not, where
it was.
Client over HTTP, per revision¶
The server is held constant down each block, so what varies is the cost of building a request and reading a reply.
Client |
Against aiohttp-tiny-mcp |
Against SDK stateless |
Against SDK session |
|---|---|---|---|
raw aiohttp, no client library |
5632 |
1034 |
1237 |
aiohttp-tiny-mcp, 2024-11-05 |
5112 |
1135 |
1326 |
aiohttp-tiny-mcp, 2025-03-26 |
5169 |
1130 |
1318 |
aiohttp-tiny-mcp, 2025-06-18 |
5225 |
1125 |
1339 |
aiohttp-tiny-mcp, 2025-11-25 |
5247 |
1124 |
1293 |
aiohttp-tiny-mcp, 2026-07-28 |
4888 |
1658 |
1656 |
official SDK, 2025-11-25 |
1465 |
613 |
652 |
Against the same server the two clients run at 5247 and 1465 calls a second:
about half a millisecond of client-side work per call. This package’s client
stays within 7-13% of a hand-written aiohttp loop.
The official client appears once rather than once per revision. It offers only
LATEST_PROTOCOL_VERSION and negotiates down, and nothing in its API asks for
another; its row is labelled with what the handshake actually settled on.
Over stdio¶
One subprocess, one pipe, no web framework on either side. Calls a second.
Pairing |
|
|
|---|---|---|
raw pipe, no protocol |
16330 |
15787 |
aiohttp-tiny-mcp 2024-11-05 client and server |
8341 |
8653 |
aiohttp-tiny-mcp 2025-03-26 client and server |
8285 |
8811 |
aiohttp-tiny-mcp 2025-06-18 client and server |
8313 |
9002 |
aiohttp-tiny-mcp 2025-11-25 client and server |
8486 |
8931 |
aiohttp-tiny-mcp 2026-07-28 client and server |
7030 |
7152 |
official SDK client -> aiohttp-tiny-mcp server |
3812 |
4094 |
aiohttp-tiny-mcp 2025-11-25 client -> SDK server |
2051 |
2070 |
official SDK client and server |
1573 |
1581 |
This is the comparison with the fewest things in it, and the ratios are the largest: against the same server the two clients are 8486 and 3812; against the same client the two servers are 8486 and 2051.
What these numbers are not¶
Not a claim about your machine. One run, one laptop, loopback, no TLS, no proxy. Compare rows within a table, never a table with someone else’s.
Not a claim about your handler. Both tools here are trivial on purpose. Real work would swamp every difference on this page, which is the honest thing to say about it: if a tool call spends 50 ms in a database, none of this matters.
Not a feature comparison. This package provides extensions, Skills, aiohttp middleware, and authentication policies. It has no built-in sampling or roots API. See how this compares with the official SDK for what each one does and does not do.
Not a distributed-system benchmark. Every server and driver runs on one machine over loopback. The benchmark does not exercise a shared
SessionStore, a sharedHub, a load balancer, a second worker, a remote database, or a question that resumes on another request. Those are the conditions this project was designed for, but measuring them requires a deployment benchmark with real backend implementations and network topology. The numbers here describe the per-request protocol overhead before application work and distributed coordination are added.Not a many-client benchmark. The HTTP rows reuse one initialized client session per server while calls are concurrent. This measures contention in a hot session; it does not model a fleet of independent clients opening and refreshing sessions at the same time.
Best-round results are optimistic. Each row is measured in several rotated rounds and the fastest round is kept. This reduces the effect of a busy laptop, but it also favors unusually quiet rounds. Treat the values as within-run comparisons, not confidence intervals or capacity guarantees.
Not a latency promise for remote-web-mcp. TLS, proxy buffering, network RTT, backend contention, open-stream lifetimes, and failover dominate a remote deployment. Use these rows to compare the two implementations under identical local conditions, then measure your actual deployment separately.
Not free of loose ends. Over HTTP the raw driver, which should be the ceiling, runs 4-10% below both client libraries against servers that frame answers as event streams, and the cause is not isolated.
benchmarks/README.mdrecords that rather than hiding it.