Skip to main content

MCP's Sticky-Session Problem Is Over: What the July 28, 2026 Stateless Spec Means for a Deploy-From-Chat MCP Server

9 min readDora NodaDora Noda
Share

In June 2025, a developer filed issue #880 against the MCP Python SDK with a title that undersold the problem: "How to actually build session persistence in streamable HTTP MCP server?" Their MCP server ran as multiple ECS tasks behind a load balancer — the standard way to run anything at scale — and clients started getting intermittent 400 Bad Request: No valid session ID provided errors. The cause: MCP's Mcp-Session-Id header pins a client to the exact server instance that issued it, and nothing in the protocol told the load balancer that. The issue sat labeled P1, because it wasn't a bug in anyone's code — it was the protocol.

That specific failure mode is going away. The 2026-07-28 MCP specification release candidate — the largest revision to the protocol since launch — removes protocol-level sessions entirely under SEP-2567. The final spec ships July 28, 2026. If you're building an MCP server that exposes real infrastructure actions — deploy, rollback, scale, tail logs — this is the change that decides whether your server runs behind a plain round-robin load balancer or needs a second stateful system just to route requests correctly.


Why Session Pinning Broke Horizontal Scaling

Under the spec MCP servers have shipped against since late 2025, a client's first request is initialize. The server responds 200 with an Mcp-Session-Id header. Every subsequent request — tools/list, tools/call, everything — must carry that same session ID back, and the server that issued it is the only server allowed to honor it:

text
client → initialize                          → server
server → 200 + Mcp-Session-Id: abc123         → client
client → tools/list  (Mcp-Session-Id: abc123) → must land on the server that owns abc123
client → tools/call  (Mcp-Session-Id: abc123) → same server instance, no exceptions

That's fine for a single process. It's a distributed-systems problem the instant you run more than one replica, because a session ID is a pointer into one server's memory, and a load balancer has no reason to know that. Teams running MCP servers in production reached for one of two fixes, and both cost something:

  • Sticky routing at the load balancer. Pin each session to one backend by cookie or header affinity. It works until that instance restarts mid-deploy, gets drained during a rolling update, or falls over under load — at which point every session pinned to it dies with it, and you've turned an ordinary instance recycle into a client-visible outage.
  • A shared session store. Move session state into Redis so any instance can serve any session. This actually solves the scaling problem, but it means running a second stateful system — with its own namespacing (you don't want every server writing to a bare session:<id> key and colliding), its own expiration policy (sliding-window TTLs tuned by hand), and its own failure mode — just to make a tool-calling API behave like the rest of your stateless HTTP fleet already does for free.

Both are workarounds for something that shouldn't have been architecturally necessary in the first place. The MCP roadmap for 2026 named this directly: transport evolution and scalability topped the list of priorities, precisely because the stateful session model breaks the load-balancer-in-front-of-N-replicas pattern every other web API takes for granted.

What Actually Changes on July 28

The release-candidate spec deletes the mechanism, not just the workaround. Mcp-Session-Id and the protocol-level session concept are both gone from Streamable HTTP. The wire exchange for the same two calls now looks like this:

text
client → tools/list                → load balancer → server A → result + ttlMs
client → tools/call                → load balancer → server B → result
                                       (different instance, same correctness)

Any request can land on any instance, because no instance owns anything the protocol cares about anymore. Three concrete mechanics make that true:

  1. No session handshake. initialize no longer establishes server affinity — there's nothing for a load balancer to accidentally break.
  2. Method-aware routing without body parsing. Streamable HTTP POST requests now carry required Mcp-Method and Mcp-Name headers, so a gateway can route or rate-limit by method (tools/call vs resources/read) without deep-packet-inspecting the JSON-RPC body — the "deep packet inspection at the gateway" that used to be necessary for anything smarter than round-robin.
  3. Explicit cache lifetimes. tools/list and resource-read responses now carry ttlMs and cacheScope fields, modeled directly on HTTP's Cache-Control: max-age. A ttlMs of 60000 means the client can treat its cached tool list as valid for 60 seconds without asking again — and cacheScope tells it whether that cache is safe to share across users or scoped to one.

Put together, the practical infrastructure change for anyone running an MCP server is: drop the sticky-session config from your load balancer, retire the shared Redis session store, and stop threading Mcp-Session-Id through your gateway routing rules. A remote MCP server can sit behind an ordinary AWS ALB or Cloudflare proxy the same way a REST API does, with no session-affinity rule anywhere in the config.

Stateless Protocol Doesn't Mean Stateless Application

This is the part worth being precise about, because "MCP went stateless" oversells it slightly: removing the protocol-level session doesn't mean your server can't have state — it means the protocol stops managing that state for you, the same deal HTTP has always offered. If a tool call genuinely needs to reference something from a prior call, you do what REST APIs have done for two decades: mint an explicit handle from the first call and have the model pass it back as an ordinary argument on the next one. A shopping-cart API doesn't ask HTTP to remember the basket; it hands back a basket_id. An MCP tool with multi-step state does the same with a deploy_id.

That pattern gets a purpose-built extension for the case that matters most to infrastructure tools: work that doesn't finish inside one request. The Tasks extension (io.modelcontextprotocol/tasks, SEP-2663) reshapes long-running operations around the stateless model instead of bolting a session back on. A tools/call can return a task handle instead of blocking for a result; the client then drives the task with tasks/get, tasks/update, and tasks/cancel — no held-open socket, no server pinned to a single instance for the operation's whole lifetime. Task creation is server-directed: the client advertises support for the extension, and the server decides which calls are worth turning into a task rather than answering inline.

That maps onto a deploy-from-chat tool almost exactly. A git push-triggered build-and-deploy takes anywhere from tens of seconds to several minutes — long enough that blocking a tools/call on it either times the client out or forces you back into the session-pinned, hold-the-socket-open pattern the rest of the spec just deleted. Tasks give you the alternative: deploy returns a handle immediately, and the calling agent polls tasks/get until it resolves — on whichever server instance happens to answer the poll.

Migration Checklist for a Deploy-From-Chat MCP Server

If you're exposing infrastructure actions — deploy, rollback, scale, fleet status — as MCP tools, here's what the July 28 spec changes about your own server, concretely:

  • Rip out session affinity. Delete the sticky-routing rule from your load balancer and the shared Redis (or equivalent) session store, once your server and client stack are both on the new spec. Both existed only to compensate for Mcp-Session-Id; once it's gone, they're dead weight and one fewer thing that can fail during a rolling deploy of your own MCP server.
  • Turn deploy and rollback into Task-returning tools. Don't model a deploy as a blocking tools/call that holds the connection for the build's full duration. Return a task handle immediately and let the agent poll tasks/get — this is also the more honest UX for a chat client, since "deploy started, here's how to check on it" is what actually happens on a git push, not instant success or failure.
  • Make workflow state an explicit argument, not implicit context. If a multi-step flow needs to remember "which app we're operating on" or "which deploy we're rolling back," pass that as a tool argument (an app_id, a deploy_id) the model carries forward — not something your server infers from which instance handled the last call, because after July 28 there's no guarantee it's the same instance.
  • Plan around the SDK timeline, not the spec date. The spec ships July 28, 2026, but there's a ten-week window for SDK maintainers to validate against real workloads. Python v2 and TypeScript v2 are the lead Tier 1 SDKs, with Go and C# betas following; if your MCP server is built on a non-Tier-1 SDK, expect your migration window to trail the spec date, not match it.
  • Adopt ttlMs on anything resembling a static tool list. If your deploy/rollback/logs tool set doesn't change per-request, set a real ttlMs on tools/list responses instead of leaving clients to re-fetch on every turn — it's a free latency win now that the spec gives you a standard field for it instead of a bespoke cache header.

None of this is exotic. It's the same lesson every stateless-HTTP API has already taught: push state to the caller as an explicit token, and let the transport layer stay dumb and horizontally scalable. MCP just spent from late 2025 to mid-2026 rediscovering it, closing the "just add MCP undersells the distributed-systems problem hiding behind a tool-calling interface" gap by removing the mechanism that created the problem instead of asking every server author to work around it individually.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with deploy, rollback, and fleet-status exposed as MCP tools an agent can call directly. Building those tools against a spec that scales on an ordinary load balancer, instead of a hand-rolled sticky-session workaround, is exactly the kind of infrastructure detail that shouldn't be your platform's problem to solve twice. Star the repo on GitHub or deploy your first app today.

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide