Skip to content

StreamableHttpClientTransport has 10x higher latency on subsequent calls vs Python SDK #789

Closed

Description

@sco3

Describe the bug

StreamableHttpClientTransport in rmcp v1.3.0 exhibits significantly higher latency on subsequent tool calls compared to the Python MCP SDK client. The first call is fast (~2ms), but every subsequent call takes ~42ms — roughly 10x slower than Python's subsequent calls (~3ms). The degradation is consistent and repro across all subsequent calls.

Client Call 1 Call 2 Call 3 Call 4 Call 5
Rust (rmcp) 1.7ms 41.2ms 42.0ms 42.9ms 41.9ms
Python 12.0ms 3.0ms 2.9ms 3.3ms 3.2ms

To Reproduce

  1. Start the servers_counter_streamhttp server from rmcp v1.3.0.
  2. Run the Rust client:
    use rmcp::transport::StreamableHttpClientTransport;
    use rmcp::{ServiceExt, model::*};
    use std::time::Instant;
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp");
        let client = ClientInfo::default().serve(transport).await?;
    
        for i in 1..=5 {
            let start = Instant::now();
            let _res = client.call_tool(CallToolRequestParams::new("say_hello")).await.unwrap();
            println!("Call {}: {:.1}ms", i, start.elapsed().as_secs_f32() * 1000.0);
        }
        client.cancel().await?;
        Ok(())
    }
  3. Run the equivalent Python client:
    import asyncio, time
    from mcp import ClientSession
    from mcp.client.streamable_http import streamablehttp_client
    
    async def main():
        async with streamablehttp_client(url="http://localhost:8000/mcp") as (read, write, _):
            async with ClientSession(read, write) as session:
                await session.initialize()
                for i in range(1, 6):
                    start = time.perf_counter()
                    await session.call_tool("say_hello", arguments={})
                    print(f"Call {i}: {(time.perf_counter() - start) * 1000:.1f}ms")
    
    asyncio.run(main())

Expected behavior

Subsequent tool calls should have similar or lower latency than the first call, as the session is already established. The Python client demonstrates this pattern (12ms → 3ms). The Rust client should not regress to ~42ms on every call after the first.

Logs

Rust client output:

Call 1: 1.7ms
Call 2: 41.2ms
Call 3: 42.0ms
Call 4: 42.9ms
Call 5: 41.9ms

Python client output:

Call 1: 12.0ms
Call 2: 3.0ms
Call 3: 2.9ms
Call 4: 3.3ms
Call 5: 3.2ms
Session termination failed: 202

Additional context

  • rmcp version: 1.3.0
  • Rust edition: 2024
  • Server: servers_counter_streamhttp from rmcp v1.3.0 samples
  • The issue is consistent across all post-initialization calls (not just a one-time spike)
  • Fedora 43, x86_64

Activity

  1. added
    bugSomething is not working
    on Apr 3, 2026
  2. sco3 commented on Apr 3, 2026

    @sco3
    Author

    The possible workaround is to set tcp no delay flag:

    use axum::{Router, routing::get, serve::ListenerExt};
    use tracing::trace;
    
    let router = Router::new().route("/", get(|| async { "Hello, World!" }));
    
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap()
        .tap_io(|tcp_stream| {
            if let Err(err) = tcp_stream.set_nodelay(true) {
                trace!("failed to set TCP_NODELAY on incoming connection: {err:#}");
            }
        });
    axum::serve(listener, router).await.unwrap();
    

    https://docs.rs/axum/latest/axum/serve/trait.ListenerExt.html#method.tap_io
    This is workaround on server side and the issue is found on client side.

  3. sco3 commented on Apr 3, 2026

    @sco3
    Author

    The additional sleep between calls improves the latency:

    use tokio::time::{sleep, Duration};  
    ...
    sleep(Duration::from_millis(40)).await;
    
  4. added
    T-transportTransport layer changes
    P1High: significant functionality gap or spec violation
    on Apr 4, 2026
  5. sco3 commented on Apr 5, 2026

    @sco3
    Author

    Workaround: Using a custom reqwest client with connection pooling disabled (pool_max_idle_per_host(0)) resolves the issue, bringing subsequent calls down to ~0.4ms.

    Client Call 1 Call 2 Call 3 Call 4 Call 5
    Rust (rmcp default) 0.6ms 42.3ms 41.0ms 41.1ms 41.1ms
    Rust (custom client) 0.5ms 0.4ms 0.4ms 0.3ms 0.4ms
    Python 9.5ms 2.8ms 2.9ms 3.0ms 2.6ms

    Custom Client

    One can build a custom client with:

    let reqwest_client = reqwest::Client::builder()
        .pool_max_idle_per_host(0)  // Disable connection pooling
        .build()?;
    
    let transport = StreamableHttpClientTransport::with_client(
        reqwest_client,
        StreamableHttpClientTransportConfig::with_uri(url),
    );

    https://github.com/sco3/mcp-rust-sdk-ticket/blob/main/src/main.rs

  6. glicht commented on Apr 7, 2026

    @glicht
    Contributor

    @sco3 this is an interesting report.

    I ran your examples and I am seeing different results. It looks to me like this may be environment specific. I am running on a macbook air and getting the following results:

    rmcp with connection pooling (default)

    Call 1: 2.1ms
    Call 2: 0.8ms
    Call 3: 0.8ms
    Call 4: 0.8ms
    Call 5: 0.8ms
    

    rmcp with custom client (no connection pooling)

    Call 1: 2.0ms
    Call 2: 1.6ms
    Call 3: 1.4ms
    Call 4: 1.3ms
    Call 5: 1.5ms
    

    python (3.13)

    Call 1: 7.1ms
    Call 2: 2.1ms
    Call 3: 2.0ms
    Call 4: 2.3ms
    Call 5: 2.4ms
    

    @DaleSeo FYI

  7. dima-zakharov commented on Apr 7, 2026

    @dima-zakharov

    Yes it is interesting, I need to check on different environments as well, maybe github workflow can help.

  8. sco3 commented on Apr 7, 2026

    @sco3
    Author

    Well the action shows the following results:

    ...
    Successfully set up CPython (3.13.12)
    ...
    Run echo "=== Rust Benchmark (Default Mode) ==="
    === Rust Benchmark (Default Mode) ===
        Finished `release` profile [optimized] target(s) in 0.11s
         Running `target/release/rust-sdk-ticket`
    Call 1: 0.7ms
    Call 2: 41.3ms
    Call 3: 41.0ms
    Call 4: 41.0ms
    Call 5: 41.0ms
    
    Run echo "=== Rust Benchmark (Custom Mode - pool_max_idle_per_host(0)) ==="
    === Rust Benchmark (Custom Mode - pool_max_idle_per_host(0)) ===
        Finished `release` profile [optimized] target(s) in 0.11s
         Running `target/release/rust-sdk-ticket custom`
    Call 1: 0.8ms
    Call 2: 0.7ms
    Call 3: 0.8ms
    Call 4: 0.7ms
    Call 5: 0.7ms
    
    Run echo "=== Python Latency Test ==="
    === Python Latency Test ===
    Session termination failed: 202
    Call 1: 10.5ms
    Call 2: 2.9ms
    Call 3: 2.9ms
    Call 4: 2.8ms
    Call 5: 3.4ms
    
    

    The full logs are attached here:
    logs_63637729286.zip

  9. sco3 commented on Apr 7, 2026

    @sco3
    Author

    The workflow file (gzipped):

    benchmarks.yml.gz

  10. sco3 commented on Apr 7, 2026

    @sco3
    Author

    I understand it may be environmental, but couple of my laptops and now the github environment show similar results.
    I updated the sample repo with pyproject.toml, pinned python version to 3.13 and created workflow to run the tests on github vm.

  11. sco3 commented on Apr 7, 2026

    @sco3
    Author

    I can confirm that on macos-14 github vm the results are similar to @glicht provided. So the issue is environmental.

    Run echo "=== Rust Benchmark (Default Mode) ==="
    === Rust Benchmark (Default Mode) ===
        Finished `release` profile [optimized] target(s) in 0.43s
         Running `target/release/rust-sdk-ticket`
    Call 1: 3.3ms
    Call 2: 0.5ms
    Call 3: 0.5ms
    Call 4: 0.2ms
    Call 5: 0.2ms
    
    

    The
    logs_63643190095.zip

  12. sco3 commented on Apr 7, 2026

    @sco3
    Author

    The check in workflow:

    Delayed ACK Policy	Macos: net.inet.tcp.delayed_ack: 3	Ubuntu: net.ipv4.tcp_low_latency: 0	
    Linux prioritizes throughput/efficiency over latency; macOS uses a dynamic algorithm.
    
  13. dima-zakharov commented on Apr 8, 2026

    @dima-zakharov

    The problem still related to mcp sdk it gives inconsistent results on different platform.

  14. DaleSeo commented on Apr 10, 2026

    @DaleSeo
    Member

    Thanks for reporting this, @sco3. I've opened #790 to fix this. Could you benchmark it against your setup on Fedora to confirm the fix works? You can test by pointing your Cargo.toml at the PR branch:

    rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk.git", branch = "fix/streamable-http-connection-reuse" }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High: significant functionality gap or spec violationT-transportTransport layer changesbugSomething is not working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions