All posts
engineering architecture infrastructure

Taking the session out of our MCP layer

Article Writer
Article Writer · Marketing
June 12, 2026 · 7 min read

We have run MCP servers in production for about eighteen months. The protocol was a session-bearing one. Each client got an Mcp-Session-Id header on its first response, and every subsequent request had to come back to the same server instance that issued it. That assumption was load-bearing across our entire integration tier. It is going away.

The 2026 MCP specification release candidate removes the protocol-level session. Any request can land on any instance. The Mcp-Session-Id header is gone. The change reads like one line in the spec. Inside our codebase, it has been a quarter of work.

The topology we had

When the session was part of the protocol, the rest of our topology followed from it. We ran a small fleet of MCP server instances behind a load balancer with consistent hashing on the session id. Each instance kept a session record in memory containing the client’s capabilities, the list of tools it had been allowed to call, the cached results of tool discovery, and a few connection-scoped subscriptions for resource updates. Periodic background sweeps evicted idle sessions after thirty minutes of silence.

The arrangement worked. It was also fragile in ways we had grown used to ignoring. Restarting an instance dropped every session it held, and clients had to re-handshake. Scaling out under load meant warming a new instance, then waiting for the next batch of sessions to drift onto it as old ones expired. Routing during a deploy required draining sessions before the instance could be cycled, which meant deploys at low-traffic hours and never on a Friday. Each of these felt like the cost of doing stateful business. We were not particularly proud of any of it.

The trade had been worth making because the alternative, before the protocol shipped a stateless variant, was to invent one ourselves. We had thought about externalising sessions to a shared store and pretending the protocol did not have them. We had not done it, because the cost of keeping that abstraction honest was high, and because the spec was still moving fast enough that the rug could be pulled in either direction. As it turned out, it was pulled in our favour.

The change in framing

Once that assumption holds, the questions worth asking about the server change. We had been writing code that asked, implicitly, “is this the right instance?” That question was baked into the load balancer config, into our session lookup, into our cache reads. Each of those was a quiet contract with the topology.

The stateless model replaces that contract with a different one. The instance does not care which session it is serving. It does not, in fact, have a session. What it has is a request, an identity attached to that request, and whatever durable state it needs to fetch to handle the request correctly. That fetch has to be fast, idempotent, and safe to do from any instance at any moment.

Reframing was the part that took the longest. The code changes themselves were small. The mental work of identifying, in every part of the server, what was secretly relying on session affinity, was not. We found assumptions buried in places we did not expect.

What we redesigned

The first piece was state we had been keeping in process. Most of it was small, and most of it was not strictly necessary in process. Tool discovery cache, capability records, per-client rate limits, authentication context cached from the last request. Each of these went into a shared store with a short TTL and an explicit invalidation path. The shared store has its own cost, but the cost is now visible. We can see how often we are hitting it. We can see the failure modes. The previous arrangement put the same cost inside a coordination problem we could not measure.

The second piece was in-memory caches we had treated as private. A few of these were genuinely instance-local optimisations, and we kept them, but we changed their meaning. They now expire on a clock, not on a session lifecycle. A request that hits an instance with a stale cache reads through to the source of truth and warms its own copy. The cache is no longer a contract. It is a hint.

The third piece was retries. Under the session model, a retry was a retry to the same instance, and the instance could reason about whether the previous attempt had partially succeeded. Under the stateless model, a retry can land anywhere, including on an instance that has no memory of the original attempt. Every operation that mutates state has to carry an idempotency key. Every handler has to check the key before acting and after acting. We had idempotency in places. We had to put it everywhere.

The fourth piece was authentication. The OAuth 2.1 alignment in the new spec is its own piece of work, and we are not finished with it. The short version is that the assumptions we had baked into our token validation, particularly around caching introspection results inside a session, no longer hold. We are moving to a model where token validation is a fast lookup against a shared cache that any instance can read, and where the worst case of a cache miss is a synchronous introspection call we have to keep within latency budget.

What still hurts

We have most of the migration in production behind a flag. The first week was the loudest. The next month was a slow grind of finding the places we had missed.

Three things still bother us.

Debugging a single request is harder. A failing call can now hit five instances over its lifetime, and the trace for it is spread across five sets of logs. Our tracing was already correlation-id driven, but the volume of context we have to pull together to understand a single failure has gone up. We have invested in better trace aggregation. It helps. It does not fully close the gap.

Attributing latency is harder. The session model gave us a single instance to point at when something was slow. The stateless model spreads the work, and the slowness of a particular request is more often a small tax across several places than a clear spike in one. We are still learning how to read that.

The auth surface is the one that worries us most. The OAuth 2.1 work is a moving target. We have a version that works for our current providers. We do not have a clean answer for the next change to the spec, and there will be one. The lesson we are taking from this migration is to assume that auth will keep moving, and to budget for it the way we budget for dependency upgrades, not the way we budget for one-off projects.

What we are not declaring

We are not done. The migration is real, the gains are real, the production behaviour is better than it was. But a redesign that touches every handler in a server we have been writing for eighteen months is not the kind of project that ends cleanly. It tapers. The last few session-shaped assumptions will surface in failures we have not had yet, and we will fix them then.

What we have learned, more than anything, is that “the protocol is stateless now” is not a change to the protocol. It is a change to every layer underneath the protocol that had been quietly using the session as a contract. The work was almost entirely in our own code. The spec just told us where to look.