DEV Community

Cover image for I Measured 27,257 MCP Connections. The p90 Session Waits 35s.
אחיה כהן
אחיה כהן

Posted on Fully Autonomous

I Measured 27,257 MCP Connections. The p90 Session Waits 35s.

In July I wrote a 56-line shell script called mcp-optional whose entire job was to remove two MCP servers from my config.

Not fix them. Remove them.

Both were stdio servers, so each one spawns its own Node process per session. With around 13 Claude Code sessions open on a 16GB M4, that was roughly 1.4GB of duplicated RAM, which meant swap thrash, which meant a hot laptop. Disabling them by default and re-enabling on demand fixed it.

I filed that under "memory problem, solved" and stopped thinking about it.

This morning, chasing something unrelated, I started reading the MCP connection logs Claude Code leaves on disk. It turns out one of those two servers wasn't just the RAM tax. It's also the slowest thing I connect to, by a wide margin — and the reason isn't its code at all.

Here's what 35 days of logs actually say.

What I measured

Claude Code writes a JSONL log per MCP server, per session, under ~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-<server>/. Two lines matter:

Starting connection with timeout of 30000ms
Connection established with capabilities: {"hasTools":true,...}
Enter fullscreen mode Exit fullscreen mode

Pair them inside one file and you get a real connection latency. I did that across every project directory on this machine:

  • 33,599 log files, 2026-08-13 → 2026-09-16 (35 days)
  • 38,876 connection attempts started
  • 27,257 clean start→established pairs, across 22 servers
  • 2,888 sessions

This is one developer's machine, not a lab. That's the point — it's the distribution you actually live in.

The headline numbers

percentile connect time
median 582 ms
p75 1,650 ms
p90 3,678 ms
p95 6,428 ms
p99 14,049 ms

The median looks great. Under six tenths of a second — nobody would ever file a bug.

But 39% of connections take longer than a second, and the p99 is fourteen seconds. Against a client timeout of 30,000ms, the tail is not a rounding error. It's most of the way to the wall.

Cause #1: it's OAuth, not your server

This is the finding that reframed the whole thing for me. I split every connection by whether a token refresh happened inside that same connection window:

n median p90
token refresh in window 748 3,170 ms 9,619 ms
no refresh 26,509 550 ms 3,400 ms

5.8× slower. Same servers, same network, same machine. The only difference is whether the client had to go get a new access token first.

Your MCP server didn't take three seconds to start. Your MCP server took 550ms to start, and an OAuth round-trip took the other 2.6 seconds while the server sat there doing nothing.

That's why "why is this server slow" is usually the wrong question. Only 2.7% of my connections hit a refresh — but that 2.7% is where a disproportionate share of the visible pain lives, because it's the path that turns a fast connection into a slow one non-deterministically. You can't reproduce it on demand, so you blame the server.

Cause #2: transport, with a twist

transport n median p90
HTTP 19,894 676 ms 3,395 ms
stdio 7,363 222 ms 4,693 ms

stdio is 3× faster at the median — no surprise, there's no network. But look at p90: stdio is worse in the tail. Spawning a process has a floor of roughly nothing and a ceiling of "npm decided to do something." An HTTP server that's already running is slower on average and far more predictable.

If you're choosing a transport, that trade is the actual decision. Not "stdio is faster."

Cause #3: nobody connects to one server

Per-connection numbers hide the thing that actually costs you time, which is fan-out. Per session on this machine:

  • median 8 MCP servers, max 16
  • median total connect time: 5.9 s
  • p90: 35.5 s
  • p99: 81.8 s
  • 33.7% of sessions spend more than 10 seconds just connecting

That p90 is the number that made me stop and re-read my own script. A third of the time I open a session, I'm waiting a meaningful fraction of a minute before the first token — not because any single server is broken, but because eight of them each drew from that fat tail and nothing amortizes.

Worst offender by median, unsurprisingly: one of the two servers I'd already disabled for RAM reasons, at 2,261 ms median and 10,413 ms at p90. I removed it in July for the wrong reason and got the right outcome.

The 29.7% nobody talks about

Of 38,876 connection attempts, 11,556 never logged an established line at all — 29.7%.

Some of that is log rotation cutting a file mid-handshake, so treat it as an upper bound rather than a failure rate. But it isn't evenly spread: a single remote connector accounts for 7,422 of them on its own. That's not noise, that's one integration failing over and over while everything upstream stays quiet about it.

The trap I almost published

My first pass had a max connect time of 119 seconds and a p99 of 14.5s, and I nearly wrote a paragraph about it.

Then I re-read the client's own log line: timeout of 30000ms. A connection cannot establish at 119 seconds if the client gives up at 30. Those samples weren't slow connections — they were a closed laptop. The wall clock kept running through sleep; the connection didn't.

So I capped every measurement at the client's declared 30s timeout and threw the rest out. That removed 24 of 27,281 samples — 0.09%. Every number above is post-cap.

I mention it because the uncapped version would have been a better story and a false one. If you run this on your own machine, cap it.

What actually helps

  1. Count your servers before you tune any of them. Median 8, p90 35 seconds. Removing one mediocre server beats optimizing a good one.
  2. Make heavy servers opt-in. That's all mcp-optional does — claude mcp remove by default, claude mcp add when a task needs it. It's the highest-leverage 56 lines I have written this year, and I wrote it for the wrong reason.
  3. Look at your refresh path, not your server code. If connections are erratic rather than uniformly slow, that's the OAuth signature: 550ms most of the time, 3.2s when the token expired.
  4. Prefer an already-running HTTP server for anything heavy. Worse median, much better p90.
  5. Read the logs you already have. Nothing here required instrumentation. It was sitting in a cache directory the whole time.

The reason I care about any of this: at Achiya Automation I run agent tooling against real client infrastructure all day, and a 35-second session start that lands on one session in ten — with a third of them over 10 seconds — is a tax I pay dozens of times a day without ever seeing it on a bill.

One question

If you've got MCP servers configured right now — how many, and when did you last check whether you still use all of them?

I'd genuinely like to know whether median-8 is normal or whether I'm the outlier. And if you've measured a refresh-vs-no-refresh split on a different client, I want to see that number, because I only have one machine to look at.

Top comments (1)

Collapse
 
jo-do profile image
Jo Do

The missing established lines deserve a competing-risk treatment rather than one failure percentage. Session termination, log rotation, timeout, and a connection still in flight all censor the same interval differently. If the logs expose close events or file timestamps, you could bound the true failure rate and plot time-to-establish with censored samples. For the user-facing metric, critical-path startup is even better than summed latency because eight connections in parallel do not cost the sum unless the client serializes them.