Sessions, State, and Scaling
Purpose and Scope
This page explains how to choose between the SDK’s default stateless HTTP serving model and the older sessionful Streamable HTTP pattern. In the v2 documentation, the preferred deployment starts with a handler factory: each request builds a fresh server instance, serves that one request, and retains no per-client server object afterward. That is the operational baseline for horizontally scaled deployments because any worker that can authenticate and parse the request can handle it. Sessions are only needed when you intentionally pin a client to a long-lived transport, need a persistent notification stream, or must preserve 2025-era behavior for existing clients.
Sources: docs/serving/sessions-state-scaling.md, docs/serving/http.md
The important distinction is that a session is not a general user account or a database record. A session pins one client to one long-lived transport instance, identified by the Mcp-Session-Id response and request header in the sessionful 2025-era transport. The 2026-07-28 revision described by the v2 serving docs is per-request and does not use that header. When you are designing scaling behavior, decide first whether the endpoint is truly per-request. If it is, keep state in external services or module-scope pools, not in the server object created for a single request.
Sources: docs/serving/sessions-state-scaling.md, docs/serving/http.md
Relevant Source Files
- docs/serving/sessions-state-scaling.md - Primary how-to for stateless defaults, enabling session IDs, routing requests by session, cleaning up transports, shutdown behavior, dropped stream concerns, and scaling considerations.
- docs/serving/authorization.md - Shows the resource-server authorization model, bearer token verification, required scopes, OAuth challenges, and how authenticated caller information is produced.
- docs/serving/express.md - Shows Express mounting, parsed body forwarding, DNS rebinding protection, and bearer auth propagation into the MCP HTTP context.
- docs/serving/fastify.md - Shows Fastify mounting, raw request and response forwarding, parsed body handling, host/origin protection, and explicit auth attachment for handlers.
- docs/serving/hono.md - Shows Hono mounting on web-standard Request objects, parsed body forwarding, runtime portability, DNS rebinding protection, and authInfo pass-through.
- docs/serving/http.md - Establishes the Streamable HTTP handler factory model, request context, shutdown entry point, notification bus, and framework-independent deployment assumptions.
Stateless HTTP Model
For new v2 deployments, start with the Streamable HTTP handler. The serving guide defines createMcpHandler as a function that takes a factory and returns a web-standard HTTP handler. The factory is called once per HTTP request, so tools, resources, and prompts should be registered inside that factory on a fresh server instance. This is different from a process-wide server object that accumulates per-user state over time. The handler can be mounted directly on web-standard runtimes or adapted to Node frameworks, but the scaling property comes from the same rule: no request depends on an in-memory server from a previous request.
Sources: docs/serving/http.md, docs/serving/express.md, docs/serving/fastify.md, docs/serving/hono.md
The factory still receives useful request context. The HTTP guide names era, authInfo, and requestInfo as context values, and its example uses authInfo to build a server whose whoami tool returns the authenticated caller. That means per-caller behavior does not require a session. You can construct a server for the authenticated caller on each request, while holding expensive shared resources such as connection pools or caches at module scope. The server instance remains cheap and short-lived; durable data belongs in your database, cache service, queue, or other storage layer that every worker can reach.
Sources: docs/serving/http.md, docs/serving/authorization.md
This model is also the safer default for multi-user deployments. If each request carries authentication and the factory builds capabilities around that caller, a load balancer can send requests to any healthy worker without losing per-user correctness. Authorization middleware or framework glue must pass the authenticated identity into the handler, but the MCP server object itself does not become the partition boundary. Partition by verified identity, tenant, resource owner, or requested resource in external storage. Do not rely on process memory to separate users unless you deliberately accept single-worker affinity and cleanup responsibilities.
Sources: docs/serving/authorization.md, docs/serving/http.md
Sessionful 2025-Era Transport
The sessionful pattern uses NodeStreamableHTTPServerTransport with a sessionIdGenerator. Supplying that generator turns sessions on; leaving it undefined is the stateless mode. During initialize, the transport returns the generated identifier in the Mcp-Session-Id response header. Later requests from the same client must include that header, and the SDK client transport sends it back automatically. One transport instance represents one session, so the server process must keep a map from session ID to transport if it wants to route future requests to the correct long-lived transport.
Sources: docs/serving/sessions-state-scaling.md
The documented route handles three request shapes. If a request contains a known session ID, it is forwarded to the stored transport. If it has no session header and the body is an initialize request, the route creates a new transport, records it when onsessioninitialized fires, connects a newly built server, and lets the transport handle the request. If a request contains an unknown session ID, the route returns a JSON-RPC error with a not found status so the client can start over. If a non-initialize request has no session header, it returns a bad request error because the existing client should resend its session identifier.
Sources: docs/serving/sessions-state-scaling.md
Cleanup is part of the contract, not an optional optimization. The session map removes an entry in transport.onclose, which fires when the client ends the session or when server code closes the transport. The shutdown tip is equally important: close every stored transport before the process exits. Closing a transport ends its server-sent event streams and rejects pending requests, which gives clients a clearer failure mode than abruptly terminating the worker. In a fleet, graceful shutdown should stop accepting new requests, close or drain known sessions, and only then let the instance disappear from service discovery.
Sources: docs/serving/sessions-state-scaling.md
Scaling and Worker-Fleet Patterns
A stateless v2 endpoint is horizontally scalable by construction: any worker with the same code, configuration, auth verifier, and access to shared backing services can serve the next request. This is the simplest worker-fleet design. Put the handler behind a load balancer, mount the same route on every instance, and keep per-user data outside the MCP server instance. The HTTP guide’s handler close method remains useful during shutdown, and its notify and bus pair are the documented surface for publishing change events to subscribed clients. That event path is separate from storing ordinary business state.
Sources: docs/serving/http.md, docs/serving/sessions-state-scaling.md
A sessionful deployment has a different scaling constraint because the session ID points to an in-memory transport instance. Later POST requests, GET notification streams, and DELETE requests must reach the worker that owns that transport. The simplest production answer is sticky routing keyed by Mcp-Session-Id after initialize. Another approach is a gateway or directory that records which worker owns each session and forwards requests accordingly. What you should not do is let every worker maintain an unrelated local map and accept any session header; a valid header on the wrong worker is indistinguishable from an unknown session unless your routing layer preserves ownership.
Sources: docs/serving/sessions-state-scaling.md
Dropped notification streams are the common reason teams keep sessionful behavior. The sessions guide frames this page as relevant when a deployment needs a dropped stream to resume or needs change notifications across nodes. Even then, state should be divided carefully. The transport owns stream lifecycle and pending protocol work, while durable application state should live in shared storage. If a worker crashes, its in-memory transport map is gone; clients may need to initialize again, and your application should be able to reconstruct user-visible state from durable data rather than from the lost transport object.
Sources: docs/serving/sessions-state-scaling.md, docs/serving/http.md
Authorization, Partitioning, and Framework Mounts
Authorization is the strongest input for safe partitioning. The serving authorization guide treats an MCP server as an OAuth resource server: it verifies access tokens issued elsewhere and does not issue them. In Express, requireBearerAuth can sit in front of the MCP route, validate required scopes, attach auth to the request, and produce OAuth WWW-Authenticate challenges for missing, expired, malformed, or insufficient tokens. The resulting AuthInfo becomes the value that request handlers can read as the HTTP auth context. Use that verified identity for tenant selection, audit logging, and resource filtering.
Sources: docs/serving/authorization.md, docs/serving/express.md
Each framework guide shows a slightly different way to preserve the same state and auth boundaries. Express uses createMcpExpressApp and passes req.body as the parsed body to the Node adapter, avoiding a second read of the consumed stream. Fastify passes request.raw, reply.raw, and request.body, and its example attaches auth to the raw request before forwarding. Hono calls handler.fetch with the raw Request and passes parsedBody and authInfo in the options object. These details matter in scaled systems because inconsistent body parsing or auth propagation can make one runtime behave differently from another even when the MCP handler code is identical.
Sources: docs/serving/express.md, docs/serving/fastify.md, docs/serving/hono.md
The framework helpers also apply DNS rebinding protection around the route. The Express, Fastify, and Hono pages all describe Host and Origin validation, with localhost defaults and explicit allowedHosts or allowedOrigins when binding publicly. This is not session state, but it is part of safe multi-user serving: a browser-originated attack should not be able to reach a local MCP endpoint just because a worker is listening. When you scale publicly, configure the hosts you serve, mount authorization before the MCP route, and keep the handler focused on protocol work instead of network trust decisions.
Sources: docs/serving/express.md, docs/serving/fastify.md, docs/serving/hono.md
Compact Reference
| Concern | Stateless handler | Sessionful transport |
|---|---|---|
| Primary API | createMcpHandler factory | NodeStreamableHTTPServerTransport |
| State location | External stores and module-scope shared clients | One in-memory transport per session |
| Session header | No Mcp-Session-Id for the 2026-07-28 per-request model | Mcp-Session-Id returned from initialize and required later |
| Load balancing | Any worker can serve any request | Route by session owner or use sticky routing |
| Cleanup | handler.close during shutdown | transport.onclose map cleanup and transport.close on shutdown |
| Auth boundary | authInfo in request context | Auth should still be checked before routing or handling |
Important sessionful callbacks and values: sessionIdGenerator enables session IDs; onsessioninitialized records a generated ID; transport.sessionId identifies the active transport after initialization; transport.onclose removes it from the map; handleRequest processes POST, GET, and DELETE traffic for that transport. Important HTTP handler values: the factory context includes authInfo, era, and requestInfo; handler.fetch serves web-standard requests; toNodeHandler adapts to Node request and response objects; handler.close supports shutdown; notify and bus publish change events to subscribed clients. Treat these surfaces as deployment contracts when designing tests and operational runbooks.
Sources: docs/serving/sessions-state-scaling.md, docs/serving/http.md
Recommended Implementation Flow
Start by implementing the stateless HTTP route. Register tools and resources inside the createMcpHandler factory, pass authentication through your chosen framework, and verify that every request succeeds when sent to any worker. Next, move durable state into shared services and use authInfo to select the caller’s partition. Only after that should you introduce the sessionful transport, and only for clients or features that truly need a long-lived notification stream or 2025-era session semantics. When you do, add explicit tests for initialize, known session routing, unknown session errors, missing session errors, DELETE cleanup, and graceful shutdown.
Sources: docs/serving/sessions-state-scaling.md, docs/serving/http.md, docs/serving/authorization.md
For related reading, use the HTTP serving page to understand the per-request handler, the Express, Fastify, and Hono pages to mount it correctly in a framework, and the authorization page to protect public routes. If you are operating a worker fleet, document the chosen routing rule beside your deployment configuration: stateless per-request load balancing, sticky session routing, or gateway forwarding. That written rule should match the protocol revision you support, because a 2026-07-28 per-request endpoint and a sessionful 2025-era endpoint have very different failure modes.