Benchmark Results

Performance measurements for the a2a-protocol-sdk Rust implementation, generated by Criterion.rs statistical benchmarking.

Note: These numbers were collected on CI runners (ubuntu-latest). Absolute values will vary by hardware. Use these for relative comparisons between operations and for regression detection across releases — not as guarantees of production performance on your specific hardware.

To reproduce on your own machine: cargo bench -p a2a-benchmarks

Last updated: 2026-09-26 10:18 UTC
Rust version: rustc 1.98.1 (48a229cea 2026-09-01)
Platform: Linux-x86_64

Interactive dashboard: See the Benchmark Dashboard for charts, visual comparisons, and drill-down analysis of these results.

Transport Throughput

End-to-end HTTP round-trip latency through JSON-RPC and REST transports. All measurements use loopback (127.0.0.1) to isolate SDK overhead from network latency.

BenchmarkMedian
transport_jsonrpc_send/single_message215.6 µs
transport_jsonrpc_stream/stream_drain224.5 µs
transport_payload_scaling/jsonrpc_send/1024226.1 µs
transport_payload_scaling/jsonrpc_send/102400515.0 µs
transport_payload_scaling/jsonrpc_send/10485762.32 ms
transport_payload_scaling/jsonrpc_send/16384285.0 µs
transport_payload_scaling/jsonrpc_send/256219.3 µs
transport_payload_scaling/jsonrpc_send/4096240.8 µs
transport_payload_scaling/jsonrpc_send/64216.7 µs
transport_rest_send/single_message212.8 µs
transport_rest_stream/stream_drain247.3 µs

Protocol Overhead

Serialization and deserialization cost per A2A type. This is the baseline tax every message pays regardless of transport.

Includes protocol/payload_scaling benchmarks that measure pure serde cost from 64B to 1MB — the correct regression detection target for serialization changes. Also compares serde_json::to_vec vs SerBuffer (thread-local reuse) and from_slice vs from_str (borrowed deserialization) paths.

BenchmarkMedian
protocol_batch/deserialize_tasks/11.2 µs
protocol_batch/deserialize_tasks/1013.4 µs
protocol_batch/deserialize_tasks/100135.1 µs
protocol_batch/deserialize_tasks/5067.7 µs
protocol_batch/serialize_tasks/1383 ns
protocol_batch/serialize_tasks/103.2 µs
protocol_batch/serialize_tasks/10030.3 µs
protocol_batch/serialize_tasks/5015.0 µs
protocol_jsonrpc_envelope/deserialize_request799 ns
protocol_jsonrpc_envelope/deserialize_response1.7 µs
protocol_jsonrpc_envelope/serialize_request254 ns
protocol_jsonrpc_envelope/serialize_response425 ns
protocol_payload_scaling/from_slice/1024544 ns
protocol_payload_scaling/from_slice/10240021.5 µs
protocol_payload_scaling/from_slice/1048576232.2 µs
protocol_payload_scaling/from_slice/163843.7 µs
protocol_payload_scaling/from_slice/256373 ns
protocol_payload_scaling/from_slice/40961.2 µs
protocol_payload_scaling/from_slice/64334 ns
protocol_payload_scaling/from_str/1024425 ns
protocol_payload_scaling/from_str/10240017.4 µs
protocol_payload_scaling/from_str/1048576182.5 µs
protocol_payload_scaling/from_str/163842.9 µs
protocol_payload_scaling/from_str/256295 ns
protocol_payload_scaling/from_str/4096928 ns
protocol_payload_scaling/from_str/64264 ns
protocol_payload_scaling/ser_buffer/1024510 ns
protocol_payload_scaling/ser_buffer/10240038.8 µs
protocol_payload_scaling/ser_buffer/1048576499.5 µs
protocol_payload_scaling/ser_buffer/163846.3 µs
protocol_payload_scaling/ser_buffer/256210 ns
protocol_payload_scaling/ser_buffer/40961.6 µs
protocol_payload_scaling/ser_buffer/64136 ns
protocol_payload_scaling/to_vec/1024574 ns
protocol_payload_scaling/to_vec/10240039.0 µs
protocol_payload_scaling/to_vec/1048576430.1 µs
protocol_payload_scaling/to_vec/163845.9 µs
protocol_payload_scaling/to_vec/256288 ns
protocol_payload_scaling/to_vec/40961.6 µs
protocol_payload_scaling/to_vec/64176 ns
protocol_stream_events/artifact_update_deserialize543 ns
protocol_stream_events/artifact_update_serialize237 ns
protocol_stream_events/status_update_deserialize353 ns
protocol_stream_events/status_update_serialize104 ns
protocol_type_serde/agent_card_deserialize1.3 µs
protocol_type_serde/agent_card_serialize511 ns
protocol_type_serde/message_deserialize/217695 ns
protocol_type_serde/message_serialize/217267 ns
protocol_type_serde/task_deserialize/2781.1 µs
protocol_type_serde/task_serialize/278398 ns

Task Lifecycle

TaskStore and EventQueue operations — the backbone of task management.

BenchmarkMedian
lifecycle_e2e/send_and_complete215.4 µs
lifecycle_e2e/stream_and_drain272.9 µs
lifecycle_queue/write_read/11.3 µs
lifecycle_queue/write_read/109.1 µs
lifecycle_queue/write_read/10088.1 µs
lifecycle_queue/write_read/5044.4 µs
lifecycle_store_get/lookup_in_1000441 ns
lifecycle_store_list/filtered_page_50_of_25028.1 µs
lifecycle_store_save/single_task844 ns

Concurrent Agents

Scaling behavior under parallel load — how latency changes as concurrency increases from 1 to 64 simultaneous operations.

BenchmarkMedian
concurrent_mixed/send_then_get347.1 µs
concurrent_sends/jsonrpc/1230.6 µs
concurrent_sends/jsonrpc/161.44 ms
concurrent_sends/jsonrpc/4473.5 µs
concurrent_sends/jsonrpc/645.73 ms
concurrent_store/save_and_get/136.8 µs
concurrent_store/save_and_get/1689.3 µs
concurrent_store/save_and_get/433.1 µs
concurrent_store/save_and_get/64232.7 µs
concurrent_streams/jsonrpc/1290.5 µs
concurrent_streams/jsonrpc/161.55 ms
concurrent_streams/jsonrpc/4475.9 µs
concurrent_streams/jsonrpc/646.25 ms

Realistic Workloads

Production-like usage patterns: multi-turn conversations, mixed payloads, interceptor chains, and connection reuse vs per-request clients.

BenchmarkMedian
realistic_complex_card/deserialize/12.2 µs
realistic_complex_card/deserialize/1012.0 µs
realistic_complex_card/deserialize/100118.1 µs
realistic_complex_card/deserialize/5059.9 µs
realistic_complex_card/serialize/1916 ns
realistic_complex_card/serialize/103.4 µs
realistic_complex_card/serialize/10027.3 µs
realistic_complex_card/serialize/5014.1 µs
realistic_connection/new_client_per_request354.9 µs
realistic_connection/reused_client216.1 µs
realistic_history_serde/deserialize/11.4 µs
realistic_history_serde/deserialize/105.3 µs
realistic_history_serde/deserialize/2010.3 µs
realistic_history_serde/deserialize/53.1 µs
realistic_history_serde/deserialize/5024.8 µs
realistic_history_serde/serialize/1591 ns
realistic_history_serde/serialize/102.1 µs
realistic_history_serde/serialize/203.8 µs
realistic_history_serde/serialize/51.3 µs
realistic_history_serde/serialize/508.4 µs
realistic_interceptor_chain/interceptors/0206.4 µs
realistic_interceptor_chain/interceptors/1209.8 µs
realistic_interceptor_chain/interceptors/10212.1 µs
realistic_interceptor_chain/interceptors/5211.2 µs
realistic_multi_turn/sequential/1226.0 µs
realistic_multi_turn/sequential/102.26 ms
realistic_multi_turn/sequential/3706.8 µs
realistic_multi_turn/sequential/51.14 ms
realistic_payload_complexity/large_metadata_10kb420.1 µs
realistic_payload_complexity/mixed_parts251.4 µs
realistic_payload_complexity/nested_metadata_10266.8 µs
realistic_payload_complexity/simple_text217.7 µs

Error Paths

Cost of error handling — comparing happy path latency to error path latency. Production systems spend significant time on error paths; benchmarking only the happy path gives an incomplete picture.

BenchmarkMedian
errors_happy_vs_error/error_path204.2 µs
errors_happy_vs_error/happy_path214.7 µs
errors_malformed_request/invalid_json99.1 µs
errors_malformed_request/wrong_content_type99.3 µs
errors_task_not_found/get_nonexistent_task117.9 µs

Streaming & Backpressure

Stream throughput under varying event volumes and consumer speeds. Reveals buffering and flow-control overhead that synthetic single-event tests miss.

The default broadcast channel capacity is 256 events (raised from 64 in v0.5.0). Deployments with >256 events/task should use EventQueueManager::with_capacity() to set a higher value.

BenchmarkMedian
backpressure_append_volume/discard_store_252_events1.43 ms
backpressure_append_volume/discard_store_27_events380.4 µs
backpressure_append_volume/discard_store_3_events250.3 µs
backpressure_append_volume/discard_store_502_events2.61 ms
backpressure_append_volume/discard_store_52_events502.6 µs
backpressure_append_volume/discard_store_7_events275.9 µs
backpressure_append_volume/sqlite_store_252_events68.24 ms
backpressure_append_volume/sqlite_store_27_events8.58 ms
backpressure_append_volume/sqlite_store_3_events2.09 ms
backpressure_append_volume/sqlite_store_502_events131.68 ms
backpressure_append_volume/sqlite_store_52_events15.16 ms
backpressure_append_volume/sqlite_store_7_events3.20 ms
backpressure_append_volume/task_store_252_events1.71 ms
backpressure_append_volume/task_store_27_events462.0 µs
backpressure_append_volume/task_store_3_events275.9 µs
backpressure_append_volume/task_store_502_events3.05 ms
backpressure_append_volume/task_store_52_events615.3 µs
backpressure_append_volume/task_store_7_events309.7 µs
backpressure_concurrent_streams/streams/1339.3 µs
backpressure_concurrent_streams/streams/162.23 ms
backpressure_concurrent_streams/streams/4634.4 µs
backpressure_slow_consumer/1ms_delay27.69 ms
backpressure_slow_consumer/5ms_delay80.53 ms
backpressure_slow_consumer/fast_consumer356.5 µs
backpressure_stream_volume/252_events1.80 ms
backpressure_stream_volume/27_events467.2 µs
backpressure_stream_volume/3_events272.4 µs
backpressure_stream_volume/502_events3.22 ms
backpressure_stream_volume/52_events631.8 µs
backpressure_stream_volume/7_events312.9 µs
backpressure_timer_calibration/sleep_1ms_actual2.10 ms
backpressure_timer_calibration/sleep_5ms_actual6.12 ms

Data Volume Scaling

TaskStore performance at realistic data volumes (1K to 100K tasks). Shows how store operations scale as data accumulates over time.

BenchmarkMedian
data_volume_concurrent_reads/get/132.7 µs
data_volume_concurrent_reads/get/1639.8 µs
data_volume_concurrent_reads/get/431.7 µs
data_volume_concurrent_reads/get/6480.2 µs
data_volume_get/lookup/1000445 ns
data_volume_get/lookup/10000447 ns
data_volume_get/lookup/100000214 ns
data_volume_history_depth/save_with_turns/12.8 µs
data_volume_history_depth/save_with_turns/107.5 µs
data_volume_history_depth/save_with_turns/2012.7 µs
data_volume_history_depth/save_with_turns/54.2 µs
data_volume_history_depth/save_with_turns/5025.7 µs
data_volume_list/filtered_page_50/100027.0 µs
data_volume_list/filtered_page_50/1000026.6 µs
data_volume_list/filtered_page_50/10000026.4 µs
data_volume_save/after_prefill/01.8 µs
data_volume_save/after_prefill/10001.3 µs
data_volume_save/after_prefill/100001.4 µs
data_volume_save/after_prefill/500001.3 µs

Memory Overhead

Heap allocation counts and bytes per operation, measured via a counting allocator (#[global_allocator]). Values represent allocation counts or bytes — not time — encoded as nanoseconds for Criterion tracking.

MetricUnit
*_alloc_countNumber of alloc() calls per operation
*_bytes_per_payloadBytes allocated per operation
BenchmarkValue
memory_bytes_per_payload/serialize_bytes/1024584
memory_bytes_per_payload/serialize_bytes/163846354
memory_bytes_per_payload/serialize_bytes/256254
memory_bytes_per_payload/serialize_bytes/40961714
memory_bytes_per_payload/serialize_bytes/64150
memory_deserialize/agent_card_alloc_count1314
memory_deserialize/task_alloc_count1045
memory_history_scaling/deserialize_allocs/11351
memory_history_scaling/deserialize_allocs/105168
memory_history_scaling/deserialize_allocs/209977
memory_history_scaling/deserialize_allocs/52970
memory_history_scaling/deserialize_allocs/5023277
memory_history_scaling/serialize_allocs/1526
memory_history_scaling/serialize_allocs/102024
memory_history_scaling/serialize_allocs/205105
memory_history_scaling/serialize_allocs/51248
memory_history_scaling/serialize_allocs/5012313
memory_serialize/agent_card_alloc_count458
memory_serialize/task_alloc_count343

Cross-Language Comparison

Rust-side criterion baselines for the workloads in benches/cross_language/. Every row below is this SDK only. The measured cross-SDK comparison — this SDK's server against the official Python SDK's server — is on Cross-Language Benchmark.

BenchmarkMedian
cross_language_concurrent_50/rust4.42 ms
cross_language_echo_roundtrip/rust232.5 µs
cross_language_minimal_overhead/rust204.6 µs
cross_language_serialize_agent_card/rust_deserialize1.3 µs
cross_language_serialize_agent_card/rust_roundtrip2.0 µs
cross_language_serialize_agent_card/rust_serialize488 ns
cross_language_stream_events/rust282.6 µs

Enterprise Scenarios

Production-scale workloads modeling real deployments: multi-tenant isolation, push notification management, eviction under memory pressure, rate limiting, CORS handling, read/write mix ratios, and large conversation histories.

BenchmarkMedian
enterprise_cancel_task/send_then_cancel344.9 µs
enterprise_client_interceptors/interceptors/0216.7 µs
enterprise_client_interceptors/interceptors/1219.5 µs
enterprise_client_interceptors/interceptors/10220.8 µs
enterprise_client_interceptors/interceptors/5219.1 µs
enterprise_cors/options_preflight89.3 µs
enterprise_eviction/save_at_capacity/100978 ns
enterprise_eviction/save_at_capacity/10001.0 µs
enterprise_eviction/save_at_capacity/100001.3 µs
enterprise_eviction/sweep_duration/100161 ns
enterprise_eviction/sweep_duration/1000163 ns
enterprise_eviction/sweep_duration/10000188 ns
enterprise_handler_limits/default_limits214.9 µs
enterprise_handler_limits/metadata_rejection132.9 µs
enterprise_handler_limits/tight_limits213.9 µs
enterprise_large_history/deserialize/10048.8 µs
enterprise_large_history/deserialize/20095.9 µs
enterprise_large_history/deserialize/500233.7 µs
enterprise_large_history/serialize/10016.2 µs
enterprise_large_history/serialize/20031.6 µs
enterprise_large_history/serialize/50080.1 µs
enterprise_large_history/store_save/10016.3 µs
enterprise_large_history/store_save/20028.7 µs
enterprise_large_history/store_save/50083.6 µs
enterprise_list_tasks/page_size/10204.6 µs
enterprise_list_tasks/page_size/25287.4 µs
enterprise_list_tasks/page_size/50426.7 µs
enterprise_multi_tenant/concurrent_tenant_saves/136.8 µs
enterprise_multi_tenant/concurrent_tenant_saves/1051.8 µs
enterprise_multi_tenant/concurrent_tenant_saves/100192.0 µs
enterprise_multi_tenant/concurrent_tenant_saves/50114.4 µs
enterprise_multi_tenant/tenant_isolation_check/1499 ns
enterprise_multi_tenant/tenant_isolation_check/10500 ns
enterprise_multi_tenant/tenant_isolation_check/100497 ns
enterprise_multi_tenant/tenant_isolation_check/50499 ns
enterprise_push_config/get257 ns
enterprise_push_config/list_per_task/1188 ns
enterprise_push_config/list_per_task/101.3 µs
enterprise_push_config/list_per_task/5011.4 µs
enterprise_push_config/set1.2 µs
enterprise_rate_limiting/no_rate_limit210.6 µs
enterprise_rate_limiting/with_rate_limit213.7 µs
enterprise_rw_mix/0r_100w294.8 µs
enterprise_rw_mix/100r_0w63.7 µs
enterprise_rw_mix/25r_75w250.7 µs
enterprise_rw_mix/50r_50w196.4 µs
enterprise_rw_mix/75r_25w129.0 µs

Production Scenarios

Full end-to-end workflows exercising the complete SDK pipeline in scenarios that real-world deployments encounter at scale: task reconnection, cold start latency, concurrent race conditions, multi-context orchestration, push config lifecycle, parallel agent bursts, and dispatch routing overhead isolation.

BenchmarkMedian
production_agent_burst/agents/103.77 ms
production_agent_burst/agents/10036.77 ms
production_agent_burst/agents/5018.01 ms
production_agent_burst_client_sharing/per_agent_client_agents/103.64 ms
production_agent_burst_client_sharing/per_agent_client_agents/10033.37 ms
production_agent_burst_client_sharing/per_agent_client_agents/5017.22 ms
production_agent_burst_client_sharing/shared_client_agents/103.17 ms
production_agent_burst_client_sharing/shared_client_agents/10029.37 ms
production_agent_burst_client_sharing/shared_client_agents/5014.88 ms
production_cancel_subscribe_race/concurrent_cancel_and_subscribe929.8 µs
production_cold_start/first_request387.6 µs
production_cold_start/steady_state223.4 µs
production_dispatch_routing/direct_handler_invoke76.5 µs
production_dispatch_routing/full_http_roundtrip214.4 µs
production_e2e_orchestration/7_step_workflow1.84 ms
production_push_config/delete_roundtrip236.9 µs
production_push_config/get_roundtrip116.1 µs
production_push_config/list_roundtrip127.2 µs
production_push_config/set_roundtrip122.1 µs
production_subscribe_to_task/send_then_subscribe396.1 µs

Advanced Scenarios

SDK capabilities exercising previously-unbenchmarked paths: tenant resolver overhead, agent card hot-reload and discovery, subscribe fan-out for reconnection bursts, streaming artifact accumulation cost, pagination full walk, and extended agent card round-trip.

BenchmarkMedian
advanced_agent_card_discovery/well_known_endpoint91.1 µs
advanced_agent_card_hot_reload/read_current_card309 ns
advanced_agent_card_hot_reload/swap_and_read656 ns
advanced_agent_card_hot_reload/swap_complex_card58.3 µs
advanced_artifact_accumulation/store_save_at_depth/0745 ns
advanced_artifact_accumulation/store_save_at_depth/101.9 µs
advanced_artifact_accumulation/store_save_at_depth/10013.2 µs
advanced_artifact_accumulation/store_save_at_depth/506.8 µs
advanced_artifact_accumulation/store_save_at_depth/50069.7 µs
advanced_artifact_accumulation/task_clone_at_depth/0129 ns
advanced_artifact_accumulation/task_clone_at_depth/101.1 µs
advanced_artifact_accumulation/task_clone_at_depth/10013.4 µs
advanced_artifact_accumulation/task_clone_at_depth/506.8 µs
advanced_artifact_accumulation/task_clone_at_depth/50067.9 µs
advanced_extended_agent_card/get_extended_card_roundtrip110.9 µs
advanced_pagination_walk/filtered/1000_tasks_page_50325.8 µs
advanced_pagination_walk/filtered/100_tasks_page_2531.9 µs
advanced_pagination_walk/unfiltered/1000_tasks_page_50637.2 µs
advanced_pagination_walk/unfiltered/100_tasks_page_2565.1 µs
advanced_subscribe_fanout/concurrent_subscribers/1658.6 µs
advanced_subscribe_fanout/concurrent_subscribers/101.16 ms
advanced_subscribe_fanout/concurrent_subscribers/5838.5 µs
advanced_tenant_resolver/bearer_resolver127 ns
advanced_tenant_resolver/bearer_resolver_with_mapper150 ns
advanced_tenant_resolver/header_resolver131 ns
advanced_tenant_resolver/header_resolver_miss89 ns
advanced_tenant_resolver/path_resolver172 ns

Agent-Level Latency Under Fault

Not run by the benchmarks workflow; no results are published here. benchmarks.yml does not run this bench, so the two groups below have no tables. To measure it locally: cargo bench -p a2a-benchmarks --bench coordinator_chain_under_fault.

End-to-end latency through a 5-hop in-process coordinator chain as the links between hops are made progressively less reliable. Unlike every other benchmark on this page, this one does not measure SDK-layer overhead — it measures the characteristic an agent-harness reviewer actually wants: "what is the end-to-end latency of an agent chain when the network between agents is unreliable, and how well do per-hop retries absorb it?"

The topology is:

test client ─[link 0]─▶ coord 1 ─[link 1]─▶ coord 2 ─[link 2]─▶ coord 3 ─[link 3]─▶ coord 4 ─[link 4]─▶ leaf

Every coordinator forwards the message to the next hop via a pre-built A2aClient wrapped in a FaultInjectingTransport. Each link applies its own independent fault profile, so per-hop faults compound end-to-end the way they would in a real deployment. Coordinators 1–4 retry their downstream call up to 3 times on retryable errors; the bench harness additionally retries the top-level send_message up to 8 times so the published error rates have effectively-zero unrecoverable-failure probability.

Honest caveats — read these before interpreting the numbers:

  • In-process, not network faults. The injected "error" is a synthetic ClientError::Timeout returned before the wrapped transport is called. This exercises the SDK's retry path faithfully, but does not exercise TCP congestion control, DNS resolution, or transport-level head-of-line blocking. Treat the numbers as "latency under SDK-level retransmission pressure," not "latency under real network loss."
  • One topology. Sequential delegation is the simplest multi-agent shape. Critic loops, parallel fan-out with deadline propagation, and plan-and-execute with replanning would be more rubric-relevant — this benchmark does not claim to cover those.
  • One benchmark does not retroactively make the other suites agent-level. It is deliberately additive: the first concrete data point in the "agent-level latency under fault" shape that the rest of the suite was missing entirely.

Group 1: per-hop latency injection (zero errors)

Varies per-link latency from 0 µs to 20 000 µs with zero synthetic errors, isolating the chain's latency-compounding factor from retry jitter. Five hops × per-hop latency gives the lower bound, plus the JSON-RPC loopback baseline (~2 ms for a five-hop chain with zero added latency).

Group 2: per-hop error injection (3 retries per hop + 8 outer retries)

Varies per-link synthetic-fault rate from 0% to 5% with zero added latency. Each coordinator retries its downstream call up to 3 times on retryable errors; the bench harness retries the top-level call up to 8 times. Records successful-path latency including retry cost, which is what "steady-state end-to-end latency under fault" means in practice.


Known Measurement Limitations

These notes help interpret benchmark results accurately and avoid misdiagnosing CI variance as real performance changes.

Streaming cross-thread scheduling

On N-core systems, `tokio::spawn` places the SSE builder task on a different worker thread with (N-1)/N probability, causing ~500µs cache-miss + work-stealing penalty. This was root-caused as the source of the ~24% bimodal distribution in all streaming benchmarks.

Mitigations (v1.0.0): The SSE builder uses `sleep` + reset (not `interval`) to eliminate timer wheel entries during active streaming. Transport streaming benchmarks use `worker_threads(1)` runtime to eliminate cross-thread variance entirely (24 high severe → 4 high mild outliers, 3× tighter confidence intervals).

Data volume get() at 100K tasks

data_volume_get/lookup reports 445 ns at 1K tasks, 447 ns at 10K and 214 ns at 100K. A 100K figure below the 1K/10K ones has previously been traced to a CPU cache warming artifact from the large populate_store() setup filling L1/L2 caches; a 4MB cache-busting step was added in v0.5.0 to flush caches between populate and measure. Where the 100K figure is still the lowest, that step has not removed the effect, and the 1K/10K numbers are the representative O(1) lookup baseline.

Stream volume per-event cost

The default broadcast channel capacity is 256 events (raised from 64 in v0.5.0). backpressure_stream_volume straddles it: 631.8 µs for 52 events, 1.80 ms for 252 and 3.22 ms for 502, so the per-event cost below and above capacity can be read directly from the table.

Production deployments expecting >256 events/task should increase EventQueueManager::with_capacity() to match their peak volume.

Transport payload insensitivity

Transport benchmarks (64B → 16KB) show a 31.5% latency increase for a 256× payload increase, because the 216.7 µs HTTP round-trip dominates. Serde regressions cannot be detected via transport benchmarks. Use the protocol/payload_scaling isolation benchmarks (64B → 1MB, pure serde) for serialization regression detection.

Connection reuse impact

Connection reuse saves 138.8 µs (39.1%) on loopback — 354.9 µs per request when the client is rebuilt each time, versus 216.1 µs when it is shared. On real networks with TLS the saving is larger still (TLS handshake dominates). Best practice: create one A2aClient at startup and share via Arc across request handlers.

Two consequences worth spelling out, because both have bitten this repo:

  • A benchmark that builds a client inside its measured region is measuring client construction, not the thing it names. production_agent_burst does exactly this, and its per-agent cost tracks 354.9 µs — the rebuild-every-time number — rather than the shared-client one.
  • Quoting this saving as a small percentage understates it by roughly 4×. It is a large fraction of a loopback request, not a rounding error.

What sharing a client is actually worth under concurrency

The bullet above says production_agent_burst tracks the rebuild-every-time number. That is true, and it invites a wrong inference: that sharing a client would move it to the reused figure. It does not, and production/agent_burst_client_sharing was added to measure the difference rather than reason about it. Both arms run the same server, the same three operations per agent, and the same burst sizes; only the client's provenance changes.

BurstClient per agentShared Arc<A2aClient>Saved per agent
10364.4 µs317.0 µs47.4 µs (13.0%)
50344.5 µs297.6 µs46.9 µs (13.6%)
100333.7 µs293.7 µs40.0 µs (12.0%)

Sharing wins at every burst size, and the medians' 95% confidence intervals are disjoint in all three, so the direction is not noise. But the size of the win is about half what the single-request comparison above predicts: 40.0 µs (12.0%) per agent against the 138.8 µs (39.1%) that reused_client versus new_client_per_request would lead you to expect.

The reason is that the two arms differ in two coupled ways, not one. A shared client skips per-agent construction and per-agent connection setup, but it also puts every concurrent agent on one connection pool, and that contention gives part of the saving back. The sequential benchmark has no contention to pay, which is why its number is the optimistic bound rather than the forecast.

The practical reading: share the client — it is free to do and wins at every size measured — but size capacity from the burst figures, not from the single-request saving.

Deserialization allocation overhead

Deserialization allocates several times more than serialization (Task: 1045 vs 343 allocs). This is inherent to serde_json's parsing model: every field creates an intermediate String/Vec allocation during parsing. The serde_helpers::deser_from_str() helper enables serde_json's borrowed-data path for ~15-25% fewer allocations. The serde_helpers::SerBuffer provides thread-local buffer reuse for serialization, eliminating the 2.3× small-payload overhead.

History depth allocation scaling

History depth scales roughly linearly: memory_history_scaling records 1351 deserialization allocs at 1 turn and 23277 at 50 turns (526 and 12313 for serialization), so a 50-turn task costs 23277 deser allocs per store.get(). The serde_helpers module provides optimized paths; for maximum throughput on deep histories, consider storing pre-serialized bytes alongside parsed structs to avoid re-parsing on every read.

Artifact accumulation clone cost

The background event processor clones the full Task struct on each SSE event. Clone cost grows with artifact count: task_clone_at_depth is 129 ns with no artifacts and 67.9 µs with 500. For tasks with 500+ accumulated artifacts, consider batching event processing or using the planned copy-on-write artifact storage (tracked as a future optimization).

Slow consumer timer calibration

The backpressure/timer_calibration benchmarks measure actual tokio::time::sleep() durations on the CI runner. On shared runners, 1ms sleep ≈ 2.10 ms actual, 5ms sleep ≈ 6.12 ms actual. Slow consumer results should be interpreted against these calibrated durations, not the nominal sleep values.

Data volume save() wide confidence intervals

The data_volume/save/after_prefill/10000 benchmark reports wide confidence intervals ([1.4µs, 3.5µs], spanning a 2.5× range) and an 18% high severe outlier rate. This is caused by BTreeSet rebalancing spikes when the sorted index crosses internal node-split thresholds during insert. The median (~1.6µs) is representative; the wide CI reflects genuine variance from the B-tree data structure, not measurement noise. This is an acceptable tradeoff: the BTreeSet enables O(page_size) pagination queries vs O(n) full scans.

Dispatch routing: direct handler vs HTTP round-trip

production/dispatch_routing/direct_handler_invoke calls the request handler directly and reports 76.5 µs; full_http_roundtrip sends a message through a JSON-RPC server over a warm keep-alive connection and reports 214.4 µs. The difference is the dispatch and transport overhead the direct call bypasses.

Subscribe fan-out scaling

advanced/subscribe_fanout reports 658.6 µs with 1 subscriber, 838.5 µs with 5 and 1.16 ms with 10.

Agent burst scaling

production/agent_burst times the whole burst: 3.77 ms for 10 agents, 18.01 ms for 50 and 36.77 ms for 100 — per agent, 377.3 µs, 360.1 µs and 367.7 µs respectively.

Cold start vs steady state

production/cold_start/first_request (387.6 µs) creates a fresh server per iteration (sample_size=20), measuring server handler initialization + first TCP connect. steady_state (223.4 µs) reuses an existing keep-alive connection, measuring the full HTTP round-trip with connection overhead already amortized. The two benchmarks measure different things — they are complementary, not comparable.

Tenant resolver overhead

Per request, the tenant resolvers cost 131 ns (header), 89 ns (header miss), 172 ns (path), 127 ns (bearer) and 150 ns (bearer with mapper), against 215.6 µs for a full JSON-RPC round trip (transport_jsonrpc_send/single_message).

Pagination context index

At 1000 tasks, the advanced/pagination_walk filtered walk takes 325.8 µs against 637.2 µs unfiltered. The BTreeSet context index reduces the scan work by only iterating tasks matching the context_id filter.


Methodology

All benchmarks use Criterion.rs, which provides:

  • Statistical significance testing — detects real regressions vs noise
  • Warm-up iterations — avoids cold-start measurement artifacts
  • Median ± MAD — robust central tendency resistant to outliers
  • Configurable sample sizes — more iterations for noisy benchmarks

Measurement rigor

All benchmarks follow these practices for reproducibility:

  • Deterministic inputs: Fixed task IDs and payloads inside iter() — no incrementing counters that change HashMap distribution across iterations
  • Setup outside measurement: Store creation, server startup, and resource allocation happen before iter(), not inside it
  • debug_assert! for invariants: Correctness checks inside measurement loops use debug_assert! to avoid string-formatting cost in release builds
  • black_box() on inputs and outputs: Prevents the compiler from eliminating measured work through dead-code optimization
  • Tolerance-based allocation assertions: Memory benchmarks use a 5% tolerance instead of exact counts to avoid spurious CI failures from serde_json/stdlib version changes
  • Side-effect interceptors: The interceptor chain benchmark uses CountingInterceptor (AtomicU64) to verify interceptors are actually invoked during measurement — not just optimized away

What we benchmark

The SDK's value proposition is the A2A protocol layer and runtime efficiency, not agent logic. The bulk of the suite therefore benchmarks what the SDK owns: transport overhead, serialization cost, store operations, concurrency scaling, streaming backpressure, error handling, and memory allocation behavior.

One benchmark — coordinator_chain_under_fault — is deliberately a different shape: it measures end-to-end agent-chain latency under fault injection, not SDK-layer overhead. It is documented in its own section above with the caveats for how to interpret it (in-process only, sequential delegation only, one topology). It is not intended to substitute for a real agent-capability benchmark suite — it closes the most obvious gap in the existing suite while staying honest about what it is.

What we do NOT benchmark

  • Agent intelligence — LLM quality is an eval problem, not a perf benchmark
  • Real network faults — the fault-injection bench simulates synthetic ClientError::Timeout responses in-process, not real packet loss or TCP congestion control
  • Network latency — all benchmarks use loopback (127.0.0.1)
  • TLS handshake — benchmarks use plaintext HTTP
  • Task completion quality — needs human-preference evaluation
  • Multi-agent topologies beyond sequential delegation — critic loops, parallel fan-out with deadline propagation, and plan-and-execute with replanning are out of scope for this crate

Reproducing locally

# Run all benchmarks
cargo bench -p a2a-benchmarks

# Run a specific module
cargo bench -p a2a-benchmarks --bench transport_throughput

# Save baseline, make changes, then compare
./benches/scripts/run_benchmarks.sh --save
# ... make changes ...
./benches/scripts/run_benchmarks.sh --compare

Full HTML reports (with violin plots and comparison overlays) are generated in target/criterion/.