skip to content

Statelessness and Scaling

Sessions, Mcp-Session-Id and Last-Event-ID replay are gone: every request stands alone and cross-call state rides in server-minted handles. Interviewers ask how you scale behind a load balancer.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

6

In MCP 2026-07-28, what replaced protocol-level sessions and the Mcp-Session-Id header?

level: middleimportance: must knowfreq 72%

answer

  1. a connection is not a conversation
  2. nothing to terminate any more
  3. context travels with the request
  4. handshake era ended at 2025-11-25
  5. no session header, no DELETE, no expiry 404

basics

~20 s

Nothing replaced them. MCP revision 2026-07-28 removed protocol sessions entirely: every request is self-contained and carries its own protocol version and capabilities, so a server must not rely on anything an earlier request on the same connection established.

solid answer

~40 s

Revision 2026-07-28 made MCP a **stateless** protocol. The spec's own wording is that every request is self-contained and carries its own protocol version and capabilities, and that servers MUST NOT rely on prior requests over the same connection to establish context — "an open connection, such as a STDIO process, is not a conversation or session." Concretely, the `initialize` / `notifications/initialized` handshake is gone (version and capabilities now ride in each request's `params._meta`), the `Mcp-Session-Id` header is no longer defined, there is no HTTP `DELETE` to terminate a session, and there is no 404-means-your-session-expired rule — a 404 now pairs with JSON-RPC `-32601`, meaning the server does not implement that method. Cross-call state did not vanish; it became explicit, in the form of server-minted handles the client passes back as ordinary tool arguments.

code

json · 13 lines
json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": { "query": "stateless" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

go deeper

for a junior

Know the headline: MCP as of revision 2026-07-28 has no sessions and no initialize handshake, and each request carries its own protocol version and capabilities. Saying that plainly already beats most memorised pre-2026 material.

for a middle

Be able to list what was actually removed — the handshake, the Mcp-Session-Id header, DELETE termination, the expired-session 404 — and to say where each piece of information went instead, namely per-request _meta and server/discover.

for a senior

Show you can operate both eras: explain that a server is Legacy, Modern or dual-era, that a modern-only client cannot talk to a handshake-era server, and how you would migrate a running deployment without breaking existing clients.

for a principal

Own the argument for the change: connection-scoped state was the thing forcing session affinity, sticky routing and drain complexity onto every MCP deployment, and the tradeoff bought is per-request overhead and explicit handle lifecycle in exchange for trivially horizontal servers.

## The model this replaced Every MCP revision up to and including 2025-11-25 opened with a handshake. The client sent an `initialize` request carrying its protocol version, its capabilities and its client info; the server answered with an `InitializeResult` containing the negotiated version, the server's capabilities, its `serverInfo` and optional `instructions`; the client then sent `notifications/initialized`. Everything that followed on that connection was interpreted against the state that exchange had created. Over Streamable HTTP the server could mint an `Mcp-Session-Id` and require the client to echo it on every subsequent request; the client could send an HTTP `DELETE` to end the session, and a `404` on a request meant the session had expired and the client had to start over with a fresh `initialize`. ## What the current revision says Revision 2026-07-28 deleted that whole layer. The spec states plainly that MCP is a stateless protocol: every request is self-contained and carries its own protocol version and capabilities, and servers MUST NOT rely on prior requests over the same connection to establish context. It goes further and denies that a connection is a conversation at all — an open stdio subprocess is a pipe that happens to carry messages, not a session. The per-request replacement lives in `params._meta` (type `RequestMetaObject`). Two keys are required on every request: `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` (an empty object `{}` is a legal capability set). `io.modelcontextprotocol/clientInfo` is optional and, like the `io.modelcontextprotocol/serverInfo` a result carries back, is self-reported and untrusted. Because capabilities travel per request, a server MUST NOT infer them from anything it saw earlier. The other half of what `initialize` used to return — `instructions`, `serverInfo`, the list of supported versions — moved to `server/discover`, a method servers MUST implement and clients MAY call. ## The concrete removals - `initialize` and `notifications/initialized`: gone. - Protocol-level sessions and the `Mcp-Session-Id` header: gone. A server that receives such a header today is receiving an unknown header with no protocol meaning. - HTTP `DELETE` as session termination: gone, along with the expired-session `404`. A modern-only server has no session to delete. - The standalone HTTP `GET` listening stream: gone, replaced by `subscriptions/listen`. - Stream resumability (`Last-Event-ID`, SSE event ids): gone. A broken stream loses the in-flight request and the client re-issues it as a new request with a new JSON-RPC id. - `ping`, and the whole `ServerRequest` union of server-initiated requests: gone (server-side input needs are expressed with `InputRequiredResult` instead). ## Where the state actually went Statelessness is a protocol property, not a claim that nothing persists. A server may absolutely own durable data — a database, a job queue, a half-built query. What changed is that continuity across calls is now **explicit and client-visible**: the server mints a handle (a workspace id, a cursor, a job id) and returns it in a tool result; the client passes it back as an ordinary argument declared in that tool's input schema. The handle is application data, not a protocol field, and the spec is emphatic that possessing one is not authentication — the threat model renamed "Session Hijacking" to "State Handle Hijacking" for exactly this reason. A related MUST reinforces the model: a server's tool, prompt and resource set MUST NOT vary per connection, though it MAY vary by the authorization presented. There is no per-connection view of the server to build up. ## Why it was done Three payoffs. First, deployment: an MCP endpoint becomes an ordinary stateless HTTP POST endpoint, so any node behind a load balancer can serve any request without session affinity, and scale-in or a rolling deploy costs at most the in-flight requests. Second, latency and simplicity: a one-shot `tools/call` no longer needs a two-round-trip handshake before it can do anything, which matters for serverless and edge deployments where a warm session was never guaranteed. Third, honesty: connection-scoped state was quietly load-bearing in implementations, and making the state explicit as handles put it where both the host and the user can see it. ## What it means in practice Building a client, you stamp `_meta` on every outgoing request rather than once at startup. Building a server, you assume nothing about who is calling beyond what this request carries — its `_meta` and its credential — and you store anything you need to remember in a place every node can reach. Reading old code or old blog posts, treat any mention of `initialize`, `Mcp-Session-Id` or `Last-Event-ID` as an artifact of the 2025-11-25-and-earlier era; a server that still speaks that way is a **Legacy**-era server, and a modern-only client cannot talk to it.

  • If there is no session to expire, what does a 404 from an MCP endpoint tell the client now?
    That the server does not implement the method it was asked for. In 2026-07-28 a `404` is paired with JSON-RPC error `-32601`, method not found. Under the older revisions a `404` could mean the `Mcp-Session-Id` had expired and the client should re-run `initialize`; that meaning is gone, so a client written against the old rule will loop pointlessly trying to re-establish a session that does not exist.
  • Does statelessness mean an MCP server may not keep a database or a cache?
    No. Statelessness constrains the protocol, not the server's storage. A server can own any durable data it likes; what it may not do is depend on earlier requests over the same connection to interpret the current one. Anything that must carry across calls is either re-sent by the client or referenced by a handle the server minted and the client passes back as a tool argument.
  • Where do a client's capabilities live now that there is no initialize result to record them?
    In `params._meta` on every single request, under `io.modelcontextprotocol/clientCapabilities` — required, and legitimately empty as `{}`. `io.modelcontextprotocol/protocolVersion` is required alongside it. The server evaluates them per request and MUST NOT infer them from anything it saw before; if a request needs a capability the caller did not declare, the server answers `-32021` `MissingRequiredClientCapabilityError` with `data.requiredCapabilities`.

The old handshake was checking into a hotel and being handed a room key. The current model is a ticket window: you state who you are and what you want at every window, and if you need to come back you carry a claim ticket the clerk gave you.

saying these in an interview costs you the question

  • Says Mcp-Session-Id is still required on every MCP POST
  • Thinks an HTTP DELETE terminates an MCP session
  • Reads a 404 as an expired session and re-runs initialize
  • Assumes the server remembers capabilities from the first request
  • Claims statelessness forbids the server from storing any data

context

open as a page

Where does cross-call state live in MCP 2026-07-28 now that sessions are gone?

level: seniorimportance: must knowfreq 58%

basics

~20 s

In explicit, server-minted handles. A tool returns an opaque identifier for whatever the server is holding, and the client passes it back as an ordinary argument on later calls — application data in the tool's own schema, not a protocol field or a header.

open as a page

If an MCP response stream breaks mid-request, how does a 2026-07-28 client recover?

level: middleimportance: should knowfreq 56%

basics

~10 s

It cannot resume. Revision 2026-07-28 removed stream resumability, so a broken stream loses the in-flight request outright and the client must re-issue the same call as a brand-new request with a new JSON-RPC id.

open as a page

Why must an MCP server never treat possession of a state handle as authentication?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Because a handle is a reference, not a credential. MCP 2026-07-28 names this threat State Handle Hijacking and states that possession MUST NOT be treated as authentication: handles pass through model context, transcripts and logs, so anyone who sees one could otherwise act as the original caller.

open as a page

What does MCP 2026-07-28 statelessness change for a remote server behind a load balancer?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Session affinity stops being required: with sessions removed, any node can serve any request, so ordinary round-robin routing works. What remains is that long-lived response streams still occupy one node, and any state a tool mints must live in shared storage.

open as a page

When should an MCP tool mint a state handle instead of taking the state in every call?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Mint a handle when the state is large, sensitive, or meaningless to the model — a handle keeps it out of the context window. Re-send it in arguments when it is small and self-describing, which avoids owning storage, expiry and cleanup for every workflow.

open as a page