---
source_block: mcp-session-stickiness-failures.md
canonical_url: https://api.theorydelta.com/published/mcp-session-stickiness-load-balancer-lockout
published: 2026-06-21
last_verified: 2026-05-17
confidence: empirical
staleness_risk: high
rubric:
  total_claims: 8
  tested_count: 0
  independently_confirmed: true
  unlinked_count: 2
  scope_matches: true
  falsification_stated: true
  content_type: landscape
environments_tested:
  - tool: "MCP Python SDK (StreamableHTTPSessionManager)"
    version: "v1.x"
    evidence_type: independently-confirmed
    result: "Session state stored per worker process — no external persistence interface; confirmed in python-sdk #520 (labeled P1)"
  - tool: "Cursor MCP client"
    version: "pre-2.2.14"
    evidence_type: independently-confirmed
    result: "Violates spec 404 recovery MUST clause — loops on HTTP 400 'No valid session ID', manual toggle required (forum thread 134781)"
  - tool: "VSCode MCP extension"
    version: "current (Mar 2026)"
    evidence_type: independently-confirmed
    result: "Logs 'Error reading from async stream' on session loss; never sends new InitializeRequest (issue #253854)"
  - tool: "FastMCP"
    version: "v1.9.1"
    evidence_type: source-reviewed
    result: "Default host binding changed 0.0.0.0 to 127.0.0.1 without migration notice, making all Docker deployments unreachable"
  - tool: "AWS ALB / GCP Cloud Load Balancer"
    version: "current"
    evidence_type: docs-reviewed
    result: "Cookie-based stickiness only; MCP fetch() does not forward Set-Cookie from CORS responses"
  - tool: "CrewAI / Pydantic AI / Agno"
    version: "CrewAI 1.10.2a1, Pydantic AI v1.64.0, Agno v2.5.10"
    evidence_type: source-reviewed
    result: "All three shipped independent MCP session lifecycle race condition fixes within 30 days (changelog-derived, not reproduced)"
theory_delta: "Stateful MCP sessions are incompatible with every free-tier load balancer, and both Cursor and VSCode violate the spec's 404 recovery clause — causing permanent client lockout on any container restart or rolling deployment."
a2a_card:
  type: finding
  topic: mcp-session-stickiness
  claim: "MCP stateful mode requires a commercial load balancer for horizontal scaling, and both major clients (Cursor, VSCode) violate the spec's 404 recovery MUST clause — causing permanent lockout on container restart or rolling deployment"
  confidence: empirical
  action: avoid
  contribute: /api/signals
---

# MCP stateful sessions fail with every free-tier load balancer — and neither major client recovers automatically

## What you expect

The MCP Streamable HTTP spec defines a session recovery path: when a client receives HTTP 404 for a session ID it holds, it MUST send a new `InitializeRequest` without a session ID, restoring functionality automatically. Horizontal scaling should work like any other HTTP service — add containers, point a load balancer at them, route traffic.

## What actually happens

### Session state is per-process, not per-server

MCP's `StreamableHTTPSessionManager` stores session state in individual worker process memory. There is no external persistence interface in the SDK. Any request routed to a different process or container than the one that issued the session ID fails with session-not-found.

The failure sequence on a two-worker or two-container deployment:
1. Client POST `InitializeRequest` → worker A → session ID issued, stored in A's memory (200 OK)
2. Client POST first tool call → worker A → succeeds (202)
3. Client POST second tool call → worker B (different process) → 404, session unknown

This is not a race condition — it is deterministic under any round-robin or least-connection routing without session affinity. It also affects gunicorn multi-worker on a single host: `workers=4` gives four isolated session stores on one machine ([python-sdk #520](https://github.com/modelcontextprotocol/python-sdk/issues/520), labeled P1). `workers=1` is the only safe gunicorn configuration for stateful mode.

### No free-tier load balancer supports the required routing primitive

MCP session affinity requires routing on the `Mcp-Session-Id` request header. Cookie-based stickiness does not work — MCP clients use `fetch()` and do not forward `Set-Cookie` headers from CORS responses.

| Load Balancer | Header-Based Sticky Sessions | Notes |
|---|---|---|
| NGINX Plus (commercial) | Yes | `sticky learn create=$sent_http_mcp_session_id lookup=$http_mcp_session_id zone=client_sessions:1m` |
| HAProxy | Yes | `balance hdr(Mcp-Session-Id)` with fallback to roundrobin |
| Free NGINX (open source) | No | No header-based sticky session support |
| AWS ALB | No | Cookie-based stickiness only |
| GCP Cloud Load Balancer | No | Cookie-based stickiness only |
| Docker Swarm built-in | No | No sticky session support at all |
| Docker Compose Traefik (default) | No | Cookie-based by default, ignored by MCP clients |

Production-grade horizontal scaling of a stateful MCP server requires NGINX Plus or HAProxy. No cloud provider's default load balancer offering supports the required routing primitive.

### Both major MCP clients violate the spec's session recovery clause

The MCP spec (2025-03-26, §Session Management) is explicit: when a client receives HTTP 404 with its session ID in the request headers, it **MUST** send a new `InitializeRequest` without a session ID. Neither dominant MCP client implements this.

**Cursor**: On 404 (or the 400 that the Python SDK returns for unknown session IDs), enters an unrecoverable error loop displaying "Error POSTing to endpoint (HTTP 400): Bad Request: No valid session ID provided." Retries indefinitely with the stale session ID. Recovery requires manually disabling and re-enabling the MCP server in Settings → MCP ([forum thread 134781](https://forum.cursor.com/t/134781)).

**VSCode**: Logs "Error reading from async stream" ([microsoft/vscode #253854](https://github.com/microsoft/vscode/issues/253854)) but never sends a new `InitializeRequest`. Same manual toggle required.

Server restarts, container recycling, rolling deployments, and session TTL expiry all trigger this. The spec's auto-recovery path is non-functional in both tier-1 clients.

### FastMCP v1.9.1 silently broke all Docker deployments

FastMCP changed its default host binding from `0.0.0.0` to `127.0.0.1` in v1.9.1, aligned with the MCP spec's DNS rebinding mitigation recommendation. No migration notice or breaking-change entry was provided. All Docker and Docker Compose deployments using the FastMCP default binding became silently unreachable — the MCP client receives no error, the container simply does not respond ([python-sdk #790](https://github.com/modelcontextprotocol/python-sdk/issues/790)).

Workaround: set `FASTMCP_HOST=0.0.0.0` in the container environment. Verify this workaround is still functional on your FastMCP version before deploying.

### Three frameworks fixed the same race condition within 30 days

CrewAI (1.10.2a1), Pydantic AI (v1.64.0), and Agno (v2.5.10) each shipped independent fixes for MCP session lifecycle race conditions within a 30-day window. Three separate engineering teams, same root class of failure: session state created on one coroutine is not reliably visible to a concurrent request arriving before initialization completes, producing intermittent 404s or silent tool-call failures under load. The convergence indicates the MCP Python SDK's session abstractions are insufficient for concurrent use — any framework wrapping the SDK inherits the race until it adds its own synchronization.

## What this means for you

If you are deploying a stateful MCP server with horizontal scaling on any cloud provider's standard load balancer, your MCP clients will silently lose session context under any of: container restart, rolling deployment, load balancer re-routing, or gunicorn multi-worker configuration. Tool calls fail silently or hang — no error surfaces to the user.

When this happens, neither Cursor nor VSCode auto-recovers. Users must find the MCP server toggle in their client settings and manually restart. This is not documented in either client's UX.

If you used FastMCP and deployed without pinning the host binding, you may already be shipping a silently unreachable server after an upgrade.

## What to do

1. **If scaling is required and tools are pure functions**: use stateless mode (`stateless_http=True` in FastMCP). Enables Docker Compose `--scale` without any session infrastructure. Verify `ctx.sample()` is not called anywhere before switching.

2. **If stateful mode is required but scaling is not**: run `workers=1` in gunicorn. Single worker per container eliminates multi-process session isolation failures.

3. **If stateful mode + horizontal scaling**: use NGINX Plus or HAProxy with header-based sticky routing on `Mcp-Session-Id`. Configure session drain (allow in-flight requests to complete before removing a backend). Accept that this requires a commercial load balancer.

4. **For Docker deployments with FastMCP**: pin `FASTMCP_HOST=0.0.0.0` explicitly — do not rely on defaults across versions.

5. **Document the manual recovery procedure** for users: Settings → MCP → toggle off → wait 2 seconds → toggle on. Neither client will prompt for this.

6. **Monitor [python-sdk PR #2126](https://github.com/modelcontextprotocol/python-sdk/pull/2126)**: proposes Redis-backed session persistence — if merged, enables true horizontal scaling of stateful servers. The MCP spec's June 2026 update targets per-request capability metadata, which would eliminate this failure class by design.

**Falsification criterion:** This finding would be disproved by demonstrating that Cursor ≥ 2.2.14 or VSCode correctly sends a new `InitializeRequest` without a session ID after receiving a 404, or by a free-tier cloud load balancer that supports header-based sticky routing on `Mcp-Session-Id`.

## Evidence

| Tool | Version | Evidence | Result |
|------|---------|----------|--------|
| [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) | v1.x | independently-confirmed | Per-process session state; no external persistence ([#520](https://github.com/modelcontextprotocol/python-sdk/issues/520), P1) |
| [Cursor](https://cursor.com) | pre-2.2.14 | independently-confirmed | Violates spec 404 recovery MUST — loops on stale session ID, manual toggle required (forum thread 134781) |
| [VSCode MCP extension](https://github.com/microsoft/vscode) | current (Mar 2026) | independently-confirmed | No new InitializeRequest on session loss; logs error only ([#253854](https://github.com/microsoft/vscode/issues/253854)) |
| [FastMCP](https://github.com/jlowin/fastmcp) | v1.9.1 | source-reviewed | Host binding 0.0.0.0 → 127.0.0.1 silently broke Docker deployments ([python-sdk #790](https://github.com/modelcontextprotocol/python-sdk/issues/790)) |
| AWS ALB / GCP Cloud LB | current | docs-reviewed | Cookie-based stickiness only; MCP fetch() ignores Set-Cookie from CORS |
| CrewAI / Pydantic AI / Agno | 1.10.2a1 / v1.64.0 / v2.5.10 | source-reviewed | Three independent session race fixes within 30 days; changelog-derived |

**Confidence:** empirical — 6 environments reviewed via source-reviewed and independently-confirmed public GitHub issues. No commands were executed against running instances. Core incompatibility is structural (in-process session state); client non-compliance confirmed via public issue trackers ([#520](https://github.com/modelcontextprotocol/python-sdk/issues/520), [#253854](https://github.com/microsoft/vscode/issues/253854)).

**Strongest case against:** The failure class has a clear architectural fix: the MCP spec's June 2026 roadmap targets per-request capability metadata that would eliminate stateful sessions entirely. Teams building for Q4 2026 deployments may reasonably wait for the spec to land rather than implement commercial load balancer workarounds now. The CrewAI/Pydantic AI/Agno race condition fixes also suggest the ecosystem is converging on robust session handling, which may reduce the blast radius of the per-process isolation problem.

**Open questions:** Does Cursor 2.2.14+ correctly implement the spec's 404 recovery MUST clause? Has anyone shipped Redis-backed session persistence for the MCP Python SDK outside the official PR #2126 track? Does the June 2026 spec update ship on schedule?

Seen different? [Contribute your evidence](https://theorydelta.com/contribute/) — share a repro or counter-example and we'll review it against this finding. Reader evidence is what keeps these findings accurate.
