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 trafficCPU spent on A2A handling — this SDKCPU spent on A2A handling — official Python SDK
10 req/s0.001 cores0.02 cores
100 req/s0.008 cores0.24 cores
1,000 req/s0.076 cores2.43 cores
10,000 req/s0.757 cores24.32 cores

Each row is the pinned CPU per request multiplied by the rate. No server was driven at these rates: the official SDK's single worker sustained 166 req/s with 50 connections in the concurrency test, so its 1,000 and 10,000 req/s rows describe several workers' worth of CPU, not the one worker measured. Unpinned, the 10,000 req/s row is 1.272 cores for this SDK against 24.21 for the official one.

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.

ServerCPU per requestRequests sampledClock-tick quantisation error
This SDK (Rust)75.7 µs28,6640.46 %
Official a2a-sdk (Python)2,432.2 µs1,2170.34 %

32.1× the CPU per request, or 2,357 µs more, on the pinned configuration described below. Unpinned, each server as its own documentation runs it, the same probe gives 127.2 µs for this SDK (19,741 requests, 0.40 % quantisation error) and 2,421.5 µs for the official SDK (1,210 requests, 0.34 %): 19.0×, or 2,294 µs more. Pinning is the more favourable configuration for this SDK; the 32.1× is the upper figure.

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.

Every latency column is the median over the 3 trials of that trial's figure, p50, p95 and p99 alike.

Note that the Rust server is slower unpinned than pinned, and the Python server is almost unaffected by pinning — it is single-threaded and CPU-bound: its p50 is 2,304.7 µs pinned and 2,218.6 µs unpinned (3.7 % lower) and its CPU per request 2,432.2 and 2,421.5 µs (0.4 % lower). The pinned configuration is the more favourable of the two for this SDK: lower latency, higher throughput, less CPU per request and a larger ratio over the Python server. It is shown because it is the like-for-like comparison, not because it is the harsher one.

pinned

pinned: rust='taskset -c 0' python='taskset -c 1' client='taskset -c 2,3'

Serverp50p95p99trial-to-trial p50 spreadthroughput (50 connections)
This SDK (Rust)70.7 µs223.0 µs299.6 µs69.1–71.1 µs7,455 req/s
Official a2a-sdk (Python)2,304.7 µs3,371.5 µs4,482.9 µs2,288.8–2,321.8 µs166 req/s
floor — no A2A work43.5 µs60.0 µs100.6 µs43.0–46.0 µsnot comparable — see below

default

No CPU pinning: every process free to run on any core, as each server's own documentation describes running it.

Serverp50p95p99trial-to-trial p50 spreadthroughput (50 connections)
This SDK (Rust)98.6 µs272.9 µs388.2 µs95.0–117.6 µs5,359 req/s
Official a2a-sdk (Python)2,218.6 µs3,095.6 µs3,968.2 µs2,215.4–2,221.9 µs168 req/s
floor — no A2A work45.0 µs62.4 µs91.6 µs44.7–45.1 µsnot 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.

The floor server runs as a thread inside the client process (a Python interpreter sharing the client's cores), not as a separate process like the two real servers, each on its own core in the pinned configuration, so floor-subtracted figures are indicative, not exact.

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, on a2a-sdk
ParameterValue
Request body181 bytes, SendMessage, A2A-Version: 1.0
Warmup per trial300 requests (discarded)
Measured per trial2,000 requests
Trials per configuration3, run back to back against one server process started once per configuration
Concurrency test50 connections × 40 requests
Percentile rulenearest-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 in history — 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

Generated2026-09-18T14:05:01Z
CPUIntel(R) Xeon(R) Processor @ 2.10GHz
Logical cores4
Kernel6.18.44-fc-v33
PlatformLinux-x86_64
Load average at run1.10, 1.01, 0.60
rustcrustc 1.94.1 (e408947bf 2026-03-25)
Python3.11.15
Build profile--release (Rust), default features of the crates at the measured commit (before OpenTelemetry instrumentation became a default feature in 0.15.0)
Repository commitb7fcabb1352a5eeebdf16a03832f09f2f5e73657
Working tree cleanyes

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

The requirements file has since moved to a2a-sdk 1.2.3 and uvicorn 0.54.0; the figures above were measured with the versions listed.

Reproducing this

Needs Linux with taskset (util-linux), at least 4 logical CPUs, python3 -m venv and network access to install the pinned Python packages.

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. All measured figures on this page are generated from them; the 1–30 s agent-turn bounds and the request rates in the first table are assumptions.