Sam Austinon October 22, 202612 min read

MCP Transports Explained: stdio vs HTTP

MCP Transports Explained: stdio vs HTTP
Contents

Figure 1: MCP Transports Explained: stdio vs HTTP — same protocol, different pipe, different failure modes

Pick the wrong MCP transport and your server works on your laptop, then falls over in production. Or it works for you and nobody else can reach it. The choice is small, but it decides who can connect, how state behaves, and what breaks when something crashes.

I read the 2026-07-28 spec and measured both transports with a small Python server (SDK v2, version 2.2.0). Here is what actually differs.

What a Transport Actually Is

The spec defines a transport as a binding: it decides how messages are framed and delivered, how cancellation works, and how the connection ends. It does not change what the messages mean. In the spec's words, "Protocol semantics are identical on every transport."

So your tools, resources and prompts behave the same either way. You change the pipe, not the server logic. The spec defines two standard transports, stdio and Streamable HTTP, and allows custom ones.

stdio Streamable HTTP
Who starts it The client, as a subprocess Independently; it stays running
Reach Same machine, same user Network, many clients
Connection stdin/stdout, newline-delimited One endpoint, POST per message
State Private per client process Shared across clients
Setup None Origin checks, auth, proxy care
Lifecycle Client closes stdin, server exits Server restarted independently

stdio: The Client Launches Your Server

With stdio, the client starts your server as a subprocess and talks to it through the standard streams. The rules are short:

  1. The server reads JSON-RPC messages from stdin and writes them to stdout.
  2. Messages are newline-delimited and "MUST NOT contain embedded newlines."
  3. The server "MUST NOT write anything to its stdout that is not a valid MCP message."
  4. Logging goes to stderr, which the client may capture, forward or ignore.
  5. Shutdown starts when the client closes your stdin. Servers "SHOULD exit promptly" when that happens.

That makes stdio the natural fit for local tools: a file helper, a database wrapper for your own machine, an editor plugin. There is no port, no network and no server to keep running. The client handles the lifecycle.

Streamable HTTP: Your Server Runs on Its Own

With HTTP, the server is "an independent process that can handle multiple client connections." It exposes one endpoint, such as https://example.com/mcp, and every message is its own POST. The server answers with a JSON object or a stream scoped to that request.

The 2026-07-28 revision changed this transport a lot. It removed:

  • protocol-level sessions,
  • the Mcp-Session-Id header,
  • the GET stream endpoint,
  • resumable streams.

Older tutorials describe those features, so do not copy them.

HTTP needs more care than stdio, because your server is reachable by others. The spec says servers MUST validate the Origin header, SHOULD bind to localhost when running locally, and SHOULD authenticate every connection. Authorization is optional overall, and the spec says stdio servers should read credentials from the environment instead of using the OAuth flow.

What I Measured

I ran both transports on one machine, localhost only, so there is no network or TLS cost in these numbers. Treat them as relative, not absolute. Run-to-run variation was real: the stdio first-answer median came out at 673 ms in one run and 736 ms in another.

Measure stdio Streamable HTTP
Startup to first answer ~0.7 s ~47 ms against a running server; 0.7–1.1 s cold start, paid once
Warm tool call (median) ~1.9 ms ~3.4 ms
20 concurrent calls, 200 ms each ~215–235 ms ~215–235 ms
Three clients each add one note ids 1, 1, 1 ids 1, 2, 3
Server killed mid-session error after ~1.1 s connection error until restart
  • Startup: a new stdio client took about 0.7 seconds to get its first answer, because each client launches a fresh Python process and imports the SDK. An HTTP client connecting to an already-running server took about 47 ms. The HTTP server's own cold start took roughly 0.7 to 1.1 seconds, but you pay that once, not per client. A server written in another language would change the stdio number.
  • Per-call speed: after warm-up, a trivial tool call had a median of about 1.9 ms over stdio and 3.4 ms over HTTP. That gap is about 1.5 ms on localhost. Over a real network, the round trip and TLS will dwarf it.
  • Concurrency: I sent 20 concurrent calls to a tool that sleeps 200 ms. Both transports finished in about 215 to 235 ms, close to the ideal 200 ms and nowhere near the 4,000 ms a serial run would take. Stdio's single shared pipe did not serialize the calls.
  • Shared state: three clients in a row each added one note. Over stdio, every client got id 1, because each client launched its own server process. Over HTTP, they got 1, 2 and 3, because they shared one process.

That last result is the one that surprises people. In my reading, it is the real dividing line: stdio gives each client a private server, and HTTP gives everyone the same one.

What Happens When the Server Dies

I killed the server mid-session and made another call on the same client:

Scenario Result
stdio, server killed The call failed with an error, after about 1.1 seconds
HTTP, server stays down The call failed with a connection error ("All connection attempts failed")
HTTP, server killed and restarted The same client object kept working, and the next call succeeded

On stdio, the spec says clients "SHOULD restart" a server that exits unexpectedly, but this SDK's client did not do that in my test. Check how your own client behaves.

The third result follows from the stateless design. The spec says as much for stdio: "Because the protocol is stateless, any in-flight requests are simply lost and the client can retry them against the fresh process." Under the old session model, a restart would have broken every connected client. Now it just costs you the in-flight requests.

Which One to Choose

Use stdio when:

  • The server runs on the same machine as the client, for one user.
  • You want zero network setup and no exposed port.
  • Private per-client state is fine, or even desirable.

Use Streamable HTTP when:

  • Many users or clients share one server.
  • The server runs remotely, behind a load balancer or gateway.
  • You need shared state, central logging, or one deployment to update.

If you pick HTTP, plan for state that lives outside the process. My demo kept notes in a Python dictionary, which only worked because it was one process. Run two copies and each would hold its own dictionary. That is my reasoning, not something I tested.

Common Pitfalls That Bite on Day One

  1. stdout is sacred on stdio. A stray print() goes into the protocol channel. In my earlier test, this SDK's client survived one, with a logged parse failure, but other clients may not. Log to stderr.
  2. Proxies buffer your stream. Behind a proxy, the spec says servers should send X-Accel-Buffering: no on streamed responses, so nginx does not buffer them.
  3. HTTP headers must match the body. The spec requires Mcp-Method, Mcp-Name and MCP-Protocol-Version headers, and servers must reject mismatches. My server returned HTTP 400 for a mismatched tool name.
  4. Binding is a default, not a decision. The SDK's HTTP runner defaults to 127.0.0.1. I read that in its function signature and did not test exposing it, so set the host deliberately.
  5. Legacy transport: HTTP+SSE is deprecated. Do not start new work on it.
  6. Copying old session code. If a tutorial mentions Mcp-Session-Id or a GET stream, it predates the 2026-07-28 revision.

Notes on Method and Limits

Credibility first, because these numbers are small:

  • One machine, localhost only. All numbers come from my runs of mcp 2.2.0 with a Python server. They exclude network latency, TLS and proxies, and vary between runs. I did not test other SDKs or servers in other languages.
  • Startup times depend heavily on the server's language and imports.
  • Crash behaviour is specific to this SDK's client. The spec says clients should restart a dead stdio server, and I saw no automatic restart here.
  • State sharing: the shared-ID result comes from my demo's single-process dictionary. The scaling claim about multiple copies is my reasoning, not a test.
  • Not tested: OAuth, deployment behind nginx, Origin checks beyond the earlier test, the SDK's stateless and session options (I saw them in the function signature only), and custom transports.
  • Spec quotes come from the 2026-07-28 stdio, transports-overview and Streamable HTTP pages I read.

Frequently Asked Questions

Should I use stdio or Streamable HTTP for my MCP server?

Choose by deployment, not by speed. Use stdio when the server runs on the same machine as the client for one user and you want no port and no network setup. Use Streamable HTTP when several users or clients share one server, when it runs remotely behind a gateway, or when you need shared state and central logging. Start local on stdio while you build, then move to HTTP deliberately once a second person needs the server.

Does the MCP transport change how my tools behave?

No. A transport is a binding: it decides how messages are framed and delivered, how cancellation works and how the connection ends. The spec states that protocol semantics are identical on every transport, so your tools, resources and prompts behave the same either way. You change the pipe, not the server logic.

Why did my MCP client break after the 2026-07-28 spec revision?

Because the Streamable HTTP transport lost a lot of machinery in that revision. Protocol-level sessions, the Mcp-Session-Id header, the GET stream endpoint and resumable streams were removed. Older tutorials and older client code still describe them. If a tool relies on session IDs or on resuming a stream after a disconnect, it needs rewriting rather than patching.

Is stdio slower than Streamable HTTP for MCP?

It is slower to start and slightly faster per call. In my measurements on one machine, a new stdio client took about 0.7 seconds to get its first answer because each client launches a fresh Python process and imports the SDK, while an HTTP client talking to an already-running server took about 47 ms. After warm-up a trivial tool call was about 1.9 ms over stdio and 3.4 ms over HTTP, a gap of roughly 1.5 ms that a real network round trip would dwarf.

Can two MCP clients share state over stdio?

No, and this is the dividing line between the transports. With stdio each client launches its own server process, so a counter kept in memory starts fresh for every client: three clients in a row each got id 1. Over HTTP one process serves everyone, so the same three clients got 1, 2 and 3. If you need shared state on HTTP, still plan to keep it outside the process, because two copies of the server each hold their own memory.

How do I secure a remote MCP server?

Bind to localhost while you develop, validate the Origin header, and authenticate every connection before you open the port. The spec requires Origin validation, recommends localhost binding for local use and recommends authentication, while leaving authorization optional overall. stdio servers should read credentials from the environment instead of running the OAuth flow. Add transport security, rate limits and per-user scoping before exposing anything publicly.

Wrapping Up

stdio and HTTP carry the same protocol, so the choice is about deployment. stdio gives each client its own local server and almost no setup. HTTP gives everyone one shared, restartable server, and asks you to handle security and external state.

Start local with stdio while you build. When a second person needs the server, move to HTTP, put your state in a database, and add authentication before you open the port. Make that move deliberately, not as an afterthought.

If you are still choosing what to expose over that transport, the MCP versus function calling comparison covers the tool-design side, and the Markdown notes server walkthrough builds a working stdio server you can point at a host.

Sources

Measurements are from mcp 2.2.0 on one machine, as described in the method notes above.

What are You Looking For?

esc