Cross-Language Benchmark: A2A Layer Cost
What this measures, and what it does not
This page answers one narrow question: what does the A2A protocol layer
itself cost to run? It compares this Rust SDK's server against the
official Python a2a-sdk server on an identical echo workload.
It is not a recommendation to change SDKs, and it is not evidence that an agent built on the official Python SDK is slow. The workload here is an echo with no application work at all, which is the case that shows protocol overhead most starkly, because there is nothing else in the request to dilute it. A real agent turn is dominated by model inference.
Put in proportion: the measured difference is 2.36 ms of CPU per request. Against an agent turn spending 1–30 s in a model, that is 0.008 %–0.24 % of the turn. No single agent will notice it. Nothing here argues otherwise.
Where it does matter is aggregation. Protocol cost is paid per request regardless of what the agent does, so it scales with traffic rather than with model latency. The same throughput costs materially different amounts of CPU:
| Sustained A2A traffic | CPU spent on A2A handling — this SDK | CPU spent on A2A handling — official Python SDK |
|---|---|---|
| 10 req/s | 0.001 cores | 0.02 cores |
| 100 req/s | 0.008 cores | 0.24 cores |
| 1,000 req/s | 0.076 cores | 2.43 cores |
| 10,000 req/s | 0.757 cores | 24.32 cores |
That is the shape of the result: irrelevant inside one agent, and the whole cost model for anything that terminates A2A traffic on behalf of many.
Headline: CPU consumed per request
The primary figure, because it is what a capacity argument actually rests on.
Latency cannot distinguish work from waiting — a server blocked on a poll and
one burning CPU can post the same round-trip time. This is the server's own
utime + stime from /proc, divided by requests served.
| Server | CPU per request | Requests sampled | Clock-tick quantisation error |
|---|---|---|---|
| This SDK (Rust) | 75.7 µs | 28,664 | 0.46 % |
Official a2a-sdk (Python) | 2,432.2 µs | 1,217 | 0.34 % |
32.1× the CPU per request, or 2,357 µs more, on the pinned configuration described below.
Latency and throughput
Both configurations are shown because either alone invites a misreading.
pinned confines each server to a single distinct core, which answers the
objection that a multi-threaded Rust server was given four cores while one
uvicorn worker got one. default runs each server exactly as its own
documentation says to.
Note that the Rust server is slower unpinned than pinned, and the Python server is unaffected by pinning — it is single-threaded and CPU-bound, so confining it changes nothing. The pinned configuration is not favourable to this SDK; if anything it is the opposite.
pinned
pinned: rust='taskset -c 0' python='taskset -c 1' client='taskset -c 2,3'
| Server | p50 | p95 | p99 | trial-to-trial p50 spread | throughput (50 connections) |
|---|---|---|---|---|---|
| This SDK (Rust) | 70.7 µs | 112.6 µs | 172.2 µs | 69.1–71.1 µs | 7,455 req/s |
Official a2a-sdk (Python) | 2,304.7 µs | 3,089.4 µs | 4,482.9 µs | 2,288.8–2,321.8 µs | 166 req/s |
| floor — no A2A work | 43.5 µs | 57.7 µs | 76.8 µs | 43.0–46.0 µs | not comparable — see below |
default
No CPU pinning: every process free to run on any core, as each server's own documentation describes running it.
| Server | p50 | p95 | p99 | trial-to-trial p50 spread | throughput (50 connections) |
|---|---|---|---|---|---|
| This SDK (Rust) | 98.6 µs | 251.9 µs | 397.4 µs | 95.0–117.6 µs | 5,359 req/s |
Official a2a-sdk (Python) | 2,218.6 µs | 3,163.1 µs | 4,035.7 µs | 2,215.4–2,221.9 µs | 168 req/s |
| floor — no A2A work | 45.0 µs | 64.9 µs | 125.1 µs | 44.7–45.1 µs | not comparable — see below |
The floor row
floor is a server that returns a pre-baked constant and does no A2A work.
It exists to separate SDK cost from the cost of the client, the loopback
stack and the kernel, which sit in every number on this page. Because that
shared cost is present in both real servers, the ratio of two end-to-end
latencies understates the ratio of the servers' own handling cost.
Subtracting the floor from the pinned p50 figures leaves 27.2 µs against 2,261.3 µs — a 83× ratio, against 32.1× on CPU. The two measure different things: floor-subtracted latency isolates protocol handling above a bare HTTP round trip, while CPU per request includes the server's share of kernel and network work. The CPU figure is the conservative one, and it is the one quoted above.
The floor's throughput is deliberately omitted. Under concurrency it measures its own crude thread-per-connection ceiling, not a floor, so it bounds nothing and must not be compared against the real servers.
Method
One shared raw-socket client sends byte-identical, pre-serialized HTTP over a warm keep-alive loopback connection to every target. Only the server differs. Driving each server with its own SDK's client would measure that SDK on both sides of the comparison and make the two legs incomparable.
Both servers already existed for interoperability testing and share an echo
contract — Echo: <text> returned as a completed task artifact. Neither was
written for this benchmark:
- this SDK:
examples/echo-agent - official SDK:
itk/agents/python-sdk/agent.py, ona2a-sdk
| Parameter | Value |
|---|---|
| Request body | 181 bytes, SendMessage, A2A-Version: 1.0 |
| Warmup per trial | 300 requests (discarded) |
| Measured per trial | 2,000 requests |
| Trials per configuration | 3, each against a freshly started server |
| Concurrency test | 50 connections × 40 requests |
| Percentile rule | nearest-rank on the sorted sample, index=int(q*n) |
The Python server runs uvicorn's fast path: uvicorn[standard] supplies
uvloop and httptools, and uvicorn's default loop/http setting of auto
selects them. The runner asserts both are installed and refuses to produce a
result without them, because measuring plain asyncio with h11 would
understate the official SDK and make this a strawman.
What remains uncontrolled
Stated so that a reader can discount the result appropriately rather than discover the caveats later.
- Response sizes still differ: 259 bytes from this SDK against 338
from the official one. The request sends
historyLength: 0, which removes the largest difference — the Python agent otherwise echoes the inbound message back inhistory— but a timestamp and artifact naming remain. Some small part of the gap is payload size, not protocol handling. - Loopback only. No real network, so this isolates SDK cost rather than reproducing deployment latency. Over a real link, both numbers move toward the network's.
- One uvicorn worker. A production Python deployment would run several behind a process manager, multiplying its throughput roughly by worker count at a proportional cost in cores and memory. The per-request CPU cost, which is the headline figure, does not improve with more workers.
- Echo workload. No model call, no I/O, no business logic. This maximises the visible share of protocol overhead by design.
- Shared container. Absolute figures are specific to the host below. Trial-to-trial spread is reported so run-to-run noise is visible.
- In-memory task stores on both sides, growing for the run's duration. Trial medians are stable across trials, so no drift was observed at this scale, but neither server was measured against a persistent store.
Environment
| Generated | 2026-09-18T14:05:01Z |
| CPU | Intel(R) Xeon(R) Processor @ 2.10GHz |
| Logical cores | 4 |
| Kernel | 6.18.44-fc-v33 |
| Platform | Linux-x86_64 |
| Load average at run | 1.10, 1.01, 0.60 |
| rustc | rustc 1.94.1 (e408947bf 2026-03-25) |
| Python | 3.11.15 |
| Build profile | --release (Rust) |
| Repository commit | b7fcabb1352a5eeebdf16a03832f09f2f5e73657 |
| Working tree clean | yes |
Python packages (full resolved set of 42 recorded in the result file):
a2a-sdk==1.1.4
httptools==0.8.0
httpx==0.28.1
pydantic==2.13.5
starlette==1.6.0
uvicorn==0.53.0
uvloop==0.22.1
Reproducing this
benches/scripts/cross_language_python.sh
benches/scripts/generate_cross_language_page.py
The runner builds both servers, pins the Python environment from
benches/requirements-cross-language.txt, runs both CPU configurations, and
writes benches/results/cross-language-{pinned,default}.json. Those files
carry the full raw distributions and complete provenance, including the
resolved dependency set and the repository commit. This page is generated
from them and contains no hand-entered figures.