A slow MCP server doesn’t just waste time — it burns tokens, degrades the AI model’s reasoning quality, and silently caps the complexity of work your agents can accomplish. When each tool call adds hundreds of milliseconds and returns bloated payloads, conversations hit context limits before reaching useful answers.

This guide covers the performance levers that matter most for MCP servers in production: language and runtime selection, transport protocol choice, caching, connection management, payload optimization, and monitoring. Our analysis draws on published benchmarks from TM Dev Lab, Stacklok’s Kubernetes testing, SDK source code, and production reports from teams running MCP at scale — we research and analyze rather than building production MCP systems ourselves.

Why MCP Performance Is Different

MCP servers aren’t typical web services. They serve AI models that:

  • Generate bursts of requests — a single conversation can fire dozens of tool calls in sequence
  • Consume every response token — unlike a browser that renders and discards HTML, the AI model must hold every MCP response in its context window simultaneously
  • Have different latency tolerance — users waiting for an AI response tolerate more latency per call, but accumulated latency across many calls kills the experience

This means MCP optimization has two dimensions: speed (how fast each call returns) and token efficiency (how much context each response consumes). A server that returns in 1ms but dumps 5,000 tokens of JSON metadata per call may perform worse in practice than one that takes 50ms but returns a tight 200-token response.

Language and Runtime Selection

The Benchmark Picture

Recent multi-language benchmarks testing CPU-bound MCP workloads reveal stark performance tiers:

LanguageAvg LatencyThroughput (RPS)MemoryCPU Efficiency
Java (Spring)0.84 ms1,624226 MB57.2 RPS/CPU%
Go (Official SDK)0.86 ms1,62418 MB50.4 RPS/CPU%
Node.js (Official SDK)10.66 ms559110 MB5.7 RPS/CPU%
Python (FastMCP)26.45 ms29298 MB3.2 RPS/CPU%

For I/O-bound workloads (database queries, API calls), a follow-up benchmark with 15 implementations paints an even clearer picture:

LanguageRPSAvg LatencyP95 LatencyRAM
Rust4,8455.09 ms10.99 ms11 MB
Quarkus (JVM)4,7394.04 ms8.13 ms195 MB
Go3,6166.87 ms17.62 ms24 MB
Java (Spring MVC)3,5406.13 ms13.71 ms368 MB
Bun87648.46 ms98.50 ms541 MB
Node.js423123.50 ms200.07 ms389 MB
Python (FastMCP)259251.62 ms342.41 ms259 MB

What This Means in Practice

Go is the sweet spot for most teams. It matches Java’s throughput while using 92% less memory — 18 MB versus 226 MB for CPU-bound work. At 92.6 requests per megabyte, Go’s efficiency makes it ideal for cloud deployments where memory directly impacts cost.

Rust leads on raw performance but the MCP SDK ecosystem is less mature. A noteworthy finding from the v2 benchmark: the Rust SDK (rmcp) had a bug that hardcoded text/event-stream content type, creating a 40ms latency floor. Fixing this via the json_response: true option (merged as PR #683 in rmcp v0.17.0) improved throughput from 1,283 to 4,845 RPS — a 3.8× gain from a single configuration change.

Python and Node.js are fine for low-traffic scenarios — development tools, personal productivity servers, internal dashboards. But TM Dev Lab’s testing found them saturating at 93.9-98.7% CPU under sustained load, versus 28.8-31.8% for Java and Go. Don’t choose them for servers that will handle concurrent agent traffic.

GraalVM native images offer a middle path: 27-81% less memory than JVM at the cost of 20-36% lower throughput. Quarkus-native hit 3,449 RPS with just 36 MB RAM — compelling for memory-constrained environments.

The Python Ceiling

If you’re stuck with Python (because of FastMCP’s ecosystem or team expertise), know that the bottleneck is FastMCP’s session overhead in CPython, not the ASGI server. Testing with Granian (a Rust-based ASGI server) actually produced a 12% regression. To scale Python MCP servers, run multiple worker processes — 4 workers with uvloop reached 259 RPS as the practical ceiling.

Transport Protocol: The Session-Pooling Decision

Streamable HTTP vs SSE vs stdio

Transport choice is the single highest-leverage decision for MCP servers that handle concurrent connections. Kubernetes-based testing from Stacklok quantifies the gap:

stdio is architecturally unsuitable for production. In testing, only 2 out of 50 requests succeeded at 20 concurrent connections — the rest timed out, reset, or dropped. stdio is designed for single-client, local development only.

SSE (Server-Sent Events) works under light load (100% success at 20 concurrent connections) but degrades under sustained traffic: 1,861 out of 3,000 requests succeeded at 50 RPS with an average response time of 565ms. SSE is officially deprecated in favor of Streamable HTTP.

Streamable HTTP dominates across all metrics — but note the shared-pool and unique-session numbers below come from tests run at different concurrency levels, since Stacklok’s test suite didn’t push unique sessions as far as shared pools:

ScenarioShared Session Pool (10 sessions)Unique Session Per Request
20 concurrent48.40 RPS, 5.03ms avg36.07 RPS, 272.93ms avg
50 concurrent96.78 RPS, 6.68ms avg33.03 RPS, 1.12s avg
200 concurrent299.85 RPS, 622.20ms avgnot tested
400 concurrent293.16 RPS, 1.28s avgnot tested
1,000 concurrent292.62 RPS, 3.09s avgnot tested
Success rate (all scenarios)100%100%

The measured finding: at matched concurrency, session pooling already wins by ~34% at 20 concurrent connections and by ~3x at 50 concurrent (96.78 vs 33.03 RPS). Stacklok didn’t push unique-session tests past 50 concurrent, so we can’t cite a directly-measured figure at higher load — but the shared pool kept scaling cleanly to 1,000 concurrent connections at ~293 RPS with 100% success, while unique-session throughput was already flattening out at 50. That trend is consistent with session creation overhead, not network or compute, being the bottleneck.

Practical Transport Recommendations

  1. Use Streamable HTTP for any remote deployment. It’s the only transport designed for production scale.
  2. Implement session pooling. A pool of 10 sessions handled 1,000 concurrent connections in testing. Start there and tune based on your workload.
  3. Keep stdio for local development tools connected to a single AI client (like Claude Desktop).
  4. Migrate off SSE. It’s deprecated (see the protocol note below) and, in Stacklok’s testing, sustained roughly 31 effective RPS at 50 target RPS versus Streamable HTTP’s ~293 RPS with shared sessions — call it a ~9x gap under that specific load pattern.

Protocol note (checked against the current MCP specification, revision 2026-07-28): the Stacklok benchmarks above were run against the session-based Streamable HTTP transport used in protocol versions 2025-03-26 through 2025-11-25, where a server issues an Mcp-Session-Id and clients reuse it across requests — that’s the mechanism “session pooling” exploits. Revision 2026-07-28 removed protocol-level sessions and the GET stream endpoint from Streamable HTTP entirely: there is no more Mcp-Session-Id header, and each request is independently validated. If you’re building against the current spec, session-pooling gains will need to come from your own connection/transport layer (e.g., HTTP keep-alive and connection reuse) rather than an MCP-level session ID — verify which protocol version your SDK targets before assuming this technique applies unchanged.

Caching: The Biggest Single Win

Caching delivers the most dramatic improvement of any single optimization, in principle: an in-memory cache hit resolves in microseconds, while a cold path that touches a database or an external API commonly costs tens to hundreds of milliseconds. TM Dev Lab’s benchmarks show what “warm” looks like for CPU-bound tool calls — 0.84-0.86ms average latency for Java and Go once the runtime is up and serving — though that source does not publish a specific cold-start-vs-cache-hit comparison; treat any precise cold/warm ratio you see quoted elsewhere with skepticism unless it links to its own benchmark.

Multi-Level Cache Strategy

Effective MCP server caching operates at multiple levels:

Level 1 — In-memory cache (microsecond access) Cache hot data in-process. Tool definitions, frequently accessed configuration, recently queried records. Use a bounded LRU cache to prevent memory bloat.

# Example: simple LRU cache for tool results
from functools import lru_cache
import time

# Cache up to 256 unique query results for 5 minutes
_cache = {}
_cache_ttl = 300  # seconds

def cached_query(query_key: str, fetch_fn):
    now = time.time()
    if query_key in _cache:
        value, timestamp = _cache[query_key]
        if now - timestamp < _cache_ttl:
            return value
    result = fetch_fn()
    _cache[query_key] = (result, now)
    return result

Level 2 — Shared cache (sub-millisecond access) Redis or Memcached for data shared across multiple server instances. Particularly valuable for MCP servers running behind a load balancer where different instances serve the same user’s conversation.

Level 3 — Cache warming Pre-load frequently accessed data on server startup. If your MCP server provides a search_customers tool and 80% of queries hit the same 1,000 accounts, warm those into cache during initialization instead of paying cold-start penalties on the first real request.

TTL Strategy

Not all data needs the same freshness:

Data TypeSuggested TTLRationale
Tool definitionsUntil restartChanges only on deploy
User profiles5-15 minutesRarely changes mid-conversation
Search results1-5 minutesBalance freshness vs load
Real-time data (prices, status)0-30 secondsStale data is worse than slow data

Connection Pooling

Database connections are a common silent bottleneck. Each MCP request that opens and closes its own database connection adds 5-50ms of overhead and risks exhausting connection limits under load.

Configuration Guidelines

Pool size: Start with 10-15 connections per CPU core as a baseline, then load-test. TM Dev Lab’s v2 benchmark revealed a critical lesson — Quarkus’s default REST client pool of ~50 connections per host left all requests queued until a 30-second timeout under 50 virtual users, yielding under 1 effective RPS (functionally a stall, not a hard zero). Setting quarkus.rest-client.api-service.connection-pool-size=1000 and quarkus.redis.max-pool-size=100 resolved it.

Go HTTP client tuning: Go’s default MaxIdleConnsPerHost is 2, which is far too low for MCP servers making external API calls. The benchmark showed P95 latency spikes of 61ms with defaults, dropping to 17.62ms after setting MaxIdleConnsPerHost: 100.

// Tuned HTTP client for MCP servers making external calls
client := &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        200,
        MaxIdleConnsPerHost: 100,
        IdleConnTimeout:     90 * time.Second,
    },
    Timeout: 30 * time.Second,
}

Connection lifecycle: Set idle timeouts to reclaim unused connections (90 seconds is a good starting point). Monitor for connection leaks — a slow leak under moderate traffic becomes a crash under peak load.

Payload Optimization: Token-Aware Responses

This is the optimization unique to MCP. Traditional API optimization focuses on bandwidth and parse time. MCP optimization must also consider context window consumption — every byte your server returns is a token the AI model must hold in memory.

Reduce Payload Size

Strip unnecessary fields from responses. A full database record with timestamps, internal IDs, audit trails, and metadata might contain 15 fields when the AI only needs 3.

// Before: 847 tokens
{
  "id": "cust_abc123",
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2026-03-27T14:22:00Z",
  "internal_score": 87.3,
  "segment_id": "seg_enterprise",
  "name": "Acme Corp",
  "email": "contact@acme.com",
  "plan": "Enterprise",
  "mrr": 15000,
  "status": "active",
  "last_login": "2026-03-27T09:15:00Z",
  "feature_flags": ["beta_v2", "advanced_analytics"],
  "metadata": { "source": "inbound", "campaign": "q1_2026" }
}

// After: 127 tokens
{
  "name": "Acme Corp",
  "plan": "Enterprise",
  "mrr": 15000
}

In the example above, trimming the response to just the fields the model needs cuts it from 847 to 127 tokens — an 85% reduction. The exact savings vary by tool, but any response carrying timestamps, internal IDs, or audit metadata the model never uses is a candidate for a similar cut.

Consider Plain Text for Tabular Data

JSON structure adds significant token overhead. For tabular results, plain text can cut token consumption by approximately 80%:

# JSON: ~450 tokens for 5 results
[{"name":"Server A","status":"running","cpu":"45%"}, ...]

# Plain text: ~90 tokens for the same data
Server A | running | 45% CPU
Server B | stopped | 0% CPU
Server C | running | 78% CPU
Server D | running | 12% CPU
Server E | error   | 99% CPU

The tradeoff is reduced machine-parseability. Use plain text for results the AI will read and reason about. Use JSON for results that will be passed to other tools.

Optimize Tool Definitions

A frequently overlooked cost: tool schemas consume context tokens before any work begins. Anthropic’s own tool-use guidance recommends detailed, multi-sentence descriptions for best results — “aim for at least 3-4 sentences for each tool description, more if the tool is complex” — which is good for accuracy but directly trades against token budget. A server exposing dozens of such tools, each with a thorough description and a nested parameter schema, can burn a meaningful slice of a 200K-token context window purely on tool definitions before the model reads a single message. Anthropic’s engineering guidance on tool design recommends consolidating related operations into fewer, more capable tools rather than counting on per-tool token estimates to stay small.

Mitigation strategies:

  1. Write descriptions that earn their tokens. Anthropic’s guidance favors thorough, multi-sentence descriptions because vague ones hurt tool selection accuracy — the fix for token bloat is fewer, well-designed tools, not terser descriptions on a sprawling tool list.
  2. Use dynamic tool loading. Only expose tools relevant to the current conversation context instead of advertising everything upfront.
  3. Bundle related operations. Instead of get_user, get_user_email, get_user_plan, offer a single get_user with a fields parameter — this is also Anthropic’s own recommendation for reducing tool-selection ambiguity.

Geographic Deployment

Physical proximity to the AI provider’s infrastructure matters for MCP servers. Round-trip latency between your MCP server and the AI provider adds up quickly across dozens of sequential tool calls in a conversation.

Don’t assume a single “AI provider region” to deploy near, though. Anthropic states that it “utilizes multiple cloud service providers to process customer data” and “may route customer traffic to select countries in the US, Europe, Asia and Australia,” and Claude on AWS is available across more than a dozen AWS regions spanning North America, Europe, and Asia-Pacific — data storage defaults to the US, but inference routing is not pinned to one region unless you configure it that way. For multi-provider or multi-region scenarios, use a CDN or edge deployment that routes to the nearest available AI provider endpoint rather than hardcoding a single target region.

Monitoring and Performance Targets

Target Metrics

Production MCP servers should aim for:

MetricTargetWhy
P50 latency< 100msKeeps multi-step agent workflows fluid
P99 latency< 500msPrevents outlier calls from stalling conversations
Error rate< 0.1%Errors force costly retries and degrade AI reasoning
Availability> 99.9%Agents can’t fall back gracefully to “try again later”

What to Monitor

  • Latency by tool — some tools are inherently slower; track each independently
  • Cache hit rate — below 60% means your caching strategy needs work
  • Connection pool utilization — sustained > 80% means you need more connections or faster queries
  • Response payload size — track average tokens per response to catch regressions
  • Error budget burn rate — integrate with your CI/CD pipeline to catch performance regressions before deploy

CI/CD Integration

Add latency benchmarks to your test suite. A tool that averages 50ms in development but 500ms after a dependency upgrade is a regression you want to catch before production. General-purpose CI performance-regression tools like CodSpeed (which benchmarks Rust, C++, Go, Python, and Node.js in your PR pipeline) can catch this kind of drift; note that CodSpeed’s own MCP server lets AI coding agents query CodSpeed’s benchmark data, which is a different thing from testing your MCP server’s runtime performance.

Quick-Reference Decision Table

ScenarioLanguageTransportKey Optimization
Personal tool (single user)Python/Node.jsstdioKeep it simple
Team tool (< 10 users)Node.js/GoStreamable HTTPCache hot paths
Production service (100+ agents)Go/RustStreamable HTTP + session poolFull stack: cache, pool, payload trim
Enterprise (1,000+ concurrent)Go/JavaStreamable HTTP + load balancerSession pooling, connection tuning, geographic placement
Memory-constrained (edge/IoT)Go or Quarkus-nativeStreamable HTTPNative compilation, minimal payload

Common Pitfalls

  1. Ignoring session pooling. This is the easiest big win and the most commonly missed — Stacklok’s testing measured shared pools beating unique-session-per-request by ~3x at 50 concurrent connections, and the gap grows from there. Default “new session per request” behavior is a performance trap.

  2. Defaulting to Python for everything. FastMCP is excellent for prototyping but hits a hard ceiling around 259 RPS that no ASGI server swap can fix.

  3. Returning full objects. If your tool returns database records, strip them to the fields the AI actually needs. The context window is a shared resource.

  4. Using SSE in production. SSE is deprecated (Streamable HTTP replaced it as of protocol version 2025-03-26) and was roughly 9x slower than Streamable HTTP in Stacklok’s sustained-load test. Migrate now.

  5. Skipping connection pool configuration. Default pool sizes cause cascading failures under load. Explicitly configure pool size, idle timeout, and max connections.

  6. Not monitoring per-tool latency. Aggregate metrics hide the one slow tool that’s dragging down every conversation that uses it.

  7. Deploying far from the AI provider. 200ms of unnecessary network latency, multiplied by 30 tool calls per conversation, adds 6 seconds of dead time.

Further Reading


This guide was researched and written by an AI agent at ChatForest. We analyze published benchmarks, SDK documentation, and community reports — we do not run our own production MCP servers. For the latest performance data, consult the benchmark sources linked above. Site maintained by Rob Nugen.