Every Stateless MCP Server Rebuilds State Somewhere

MCPAPI DesignArchitectureServerlessEdge Computing

July 28, 2026

Three torn-paper vessels side by side, each holding an identical small cube, with the middle vessel lit warm amber

The 2026-07-28 specification deleted the initialize handshakeinitialize handshakeThe one-time exchange at the start of an MCP connection where a client and server negotiated protocol version and capabilities before any tool calls happened.See also MCP, Mcp-Session-Id header, protocol version field and the Mcp-Session-Id header. It did not delete one byte of what they carried. A stateless MCP server still holds that state somewhere, and the only decision left is which of three places absorbs it.

The handshake moved onto every request#

Most people read the removal of the MCP session handshake as a deletion. Read the request schema instead. Protocol version, client identity and client capabilities were negotiated once at initialize, and they are now mandatory fields inside every single request's _meta.

  • protocolVersion, negotiated once at startup before, now restated on every single call
  • clientInfo, the name and version string, repeated forever
  • clientCapabilities, the field the server is explicitly forbidden from caching
{
  "_meta": {
    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
    "io.modelcontextprotocol/clientInfo": { "name": "claude-code", "version": "2.1.0" },
    "io.modelcontextprotocol/clientCapabilities": {
      "roots": { "listChanged": true }, "sampling": {}, "elicitation": {}
    }
  }
}

That block rides along forever, because the spec forbids the server from remembering it. SEP-2575 is blunt about the consequence. "servers MUST NOT infer capabilities from prior requests."

So the state did not evaporate. It became payload, and payload has a price you can measure.

A minimal tools/call built to the SEP-2575 schema, with its required HTTP header, weighs 393 bytes. The same call under 2025-06-18 weighed 163. That is 230 extra bytes on every request, a 141% increase, to carry what one handshake used to carry once.

Cumulative request bytes, handshake versus per-request metadata

The stateless protocol costs more on the wire from the very first call
Category2025-06-18 handshake (bytes)2026-07-28 stateless (bytes)
1 call382393
10 calls18493930
100 calls1651939300
1000 calls163219393000

The handshake it replaced cost 219 bytes, paid a single time. Break-even lands at 0.95 calls. The stateless protocol is already the more expensive one before the first response comes back.

That exact percentage will not be yours. It scales with how fat your clientInfo and clientCapabilities objects are, so measure your own payload rather than quoting this one.

const bytes = (o) => Buffer.byteLength(JSON.stringify(o), "utf8");
console.log(bytes(yourCallWithMeta) - bytes(yourCallWithoutMeta));

Subscriptions became the client's bookkeeping#

The same box of watched resource URIs drawn twice, once inside a container marked server and once inside a container marked client, with a red arrow curving back from client to server labelled re-send on reconnect and the server's subscribe door crossed out in red.
The ledger of watched resources is identical in both places, only its owner changed.

MCP resource subscriptionsresource subscriptionAn MCP feature where a client registers interest in specific resource URIs so it gets notified of changes. It was removed from the stateless spec because tracking subscribers required the server to remember state.See also STDIO transport, subscriptions/listen are gone, and the spec names the reason without flinching. "Resource subscriptions are inherently stateful", it says, because the server "must remember which resources each client has subscribed to".

Read that again. The feature was not removed because nobody wanted it. It was removed because it required somebody to remember something, and the server was no longer allowed to be that somebody.

The remembering still happens. A client now declares every resource URI it cares about in the params of a subscriptions/listen request, which means the client holds the list. On STDIOSTDIO transportA way of connecting an MCP client and server over standard input and output streams rather than HTTP, where the spec requires clients to re-send subscriptions after a server crash.See also resource subscription, subscriptions/listen the spec hands over recovery too, requiring the client to re-send the whole subscription request when a server crashes and restarts.

Your server got simpler. The system did not.

Tasks replaced the state store it removed#

Resumable SSE streams died in the same release, for the same reason. Resuming a dropped stream would need the server to retain per-request state across the failure, which is exactly what stateless-by-default forbids.

Removing resumability leaves a hole, and durable work still has to survive a dropped connection. The spec fills the hole by pointing at a different stateful thing. Workloads needing durability or resumability must use the tasks primitivetasks primitiveA poll-based mechanism in MCP for durable work, letting a client fetch results after a dropped connection instead of resuming a live stream. It replaced resumable SSE streams as the way to survive a failure.See also elicitation, harness, poll-based task store instead, which fetches results after a drop.

Tasks graduated from experimental to an official extension in this release, with a poll-based tasks/get and a new tasks/update. A poll-based task storepoll-based task storeA durability mechanism where a client repeatedly checks a status endpoint for a task's result instead of holding an open connection, used by MCP's tasks primitive.See also resumable SSE stream, tasks primitive is a state store that happens to have a spec number.

The renaming goes further. SEP-2567SEP-2567The MCP proposal to remove sessions entirely and replace them with explicit state handles that a tool mints and the model passes back as an argument.See also SEP-2575, state handle proposes removing sessions entirely and replacing them with explicit state handlesstate handleAn explicit token a tool mints and hands back to the model to pass in as an argument later, replacing the implicit session concept that stateless MCP forbids servers from keeping.See also Durable Object, Mcp-Session-Id header, SEP-2567, and the official release post tells implementers to "mint an explicit handle from a tool and have the model pass it back as an argument". The word session retired. The concept got a new job title.

The session-free server still pays#

The strongest objection to all of this is that a genuinely session-free server now exists in production, and it does. Cloudflare's Agents SDK v0.20.0 serves "tools, prompts, resources, and elicitationelicitationA multi-round-trip MCP request pattern where a server pauses a tool call to ask the client for more input, then retries the original operation once that input arrives.See also callTool promise, multi-round-trip request, tasks primitive without an MCP transport session or Durable ObjectDurable ObjectA Cloudflare Workers primitive that gives a single instance of code its own persistent storage and guaranteed single-threaded execution, used to hold state a stateless server can't.See also McpAgent, state handle". Its previous stateful primitive, McpAgent, is now deprecated and feature-frozen.

No session. No storage object. That is the real counterexample, and it deserves better than a hand-wave.

It relocates state twice anyway. Every request carries the full _meta block from the first section, which is the wire absorbing it. And elicitation runs through multi-round-trip requests, where the SDK collects the input, retries the original operation, then resolves the original callTool promise.

Look at what that retry implies. A half-finished operation existed across two round trips, and the server was forbidden from holding it, so the client held it. The server did not become stateless. It became the part of the system that forgot.

Here is the honest limit of the argument. A pure tools/call server with no auth, no pagination and no elicitation genuinely holds nothing between requests, and it pays for that with 230 bytes of re-sent identity per call. Even the simplest case buys statelessness rather than getting it free.

Pick where the state lands#

A box marked state dropping out of a server and falling along three arrows into three open containers labelled the wire, the client and a store, each holding an identical copy of the same state box, with the store drawn in amber.
Three destinations, and the state box arrives intact in every one of them.

Three destinations, and the spec is neutral about which you choose.

  • The wire, cheapest to build, and it bills you per request forever. Fine for low call volume, bad for chatty agents.
  • The client, free for you, and it moves the failure into somebody else's reconnect logic. Fine when you control the client.
  • A store such as tasks, a handle, Redis or a Durable Object. Costs real infrastructure and is the only one that survives a crash.

The migration guides going around this week are checklists for the header changes. They are correct and they are not the decision. If you are porting a server, the parts nobody explains matter less than answering which of those three rows your state moved into, because you already picked one by accident.

What would change this position is a real deployment, running any session-scoped feature, where no component anywhere holds more than it did before. Not a benchmark. An architecture. Nobody has shown one yet, and the round-trip math suggests why that is hard.

Stateless is a statement about the protocol. It was never a statement about your system.

Share

Newsletter

New posts land in your inbox when they publish. No spam, unsubscribe anytime.

Prefer RSS