# When must an instance abandon header-based MCP session affinity in favor of stateless transport fallback at the edge **Date of Record:** 2026 November 8 **Scope:** Applies to MCP (Model Context Protocol) server deployments behind edge proxies/CDNs that route requests to backend instances using a session-identifying header (e.g., `Mcp-Session-Id`) to pin a client to the instance holding its session state. Does not apply to single-instance deployments, to transports that natively carry full state per request (e.g., stateless JSON-RPC over HTTP with no server-side session), or to long-lived SSE/WebSocket connections where the transport itself maintains the binding. ## Core Distinction The distinction is between *affinity as an optimization* and *affinity as a correctness requirement*. Header-based session affinity is safe to drop when the backend can reconstruct or reject-without-corruption any request that lands on a non-owning instance. It is *not* safe to drop when the session header is the sole carrier of state that the instance mutates non-idempotently — for example, an in-flight `tools/call` whose partial side effects (a half-written file, a reserved resource, an incremented counter) exist only in the owning instance's memory. The non-obvious boundary is that the edge cannot tell these two cases apart from the header alone: both present an `Mcp-Session-Id` that resolves to an instance that is *not* the one receiving the request. The failure mode is not "request goes to wrong instance" — it is "request goes to wrong instance and the fallback path *silently succeeds* with a fresh, empty session," producing a 200 OK that masks lost state. ## What Was Observed During a regional edge failover, the affinity layer (consistent-hash on `Mcp-Session-Id`) rehashed because the backend pool lost two of six instances. Approximately 3% of active sessions rehashed onto instances that had never seen them. The stateless fallback path — designed for clients that send no session header — activated for these requests because the receiving instance found no matching session record and treated the header as advisory. Observed consequences, in order of detection latency: 1. `tools/list` returned the correct catalog (stateless, harmless) — this masked the problem. 2. `tools/call` for a stateful tool (`session_append`) created a *new* empty session and appended to it, returning success. The client's prior context was orphaned on the dead instance. 3. A subsequent `resources/read` on the client's expected URI returned `-32602 Invalid params` because the resource existed only in the orphaned session. The critical observation: the fallback was *correct by its own contract* (stateless requests get stateless handling) but *wrong by the session's contract* (a session-bearing request must not be silently downgraded). The 200 OK on step 2 delayed detection by roughly 40 seconds of client retries before the `resources/read` failure surfaced the divergence. ## Condition of Validity The stateless fallback is valid **if and only if** the receiving instance can prove one of the following for the presented `Mcp-Session-Id`: - The session is *provably absent cluster-wide* (not merely absent locally), meaning no other instance holds it and no in-flight mutation could be pending; or - The request is *idempotent and self-contained* (declared so by the tool/resource metadata), so re-execution on a fresh session is semantically equivalent; or - The session state is *externalized* to a shared store keyed by the session ID, so the receiving instance can rehydrate it. If none hold, the fallback is invalid and the request must be rejected with an explicit, retryable error that forces the client to re-establish the session — never silently served on a fresh session. The boundary is therefore: **affinity may be abandoned only when absence is proven, not when it is merely local.** ## Decision Boundary The instance must reject rather than fall back when all of the following are true: - The request carries a session header. - The local instance has no record of that session. - The tool/resource is marked non-idempotent or state-mutating. - Cluster-wide absence cannot be confirmed within the request's deadline (e.g., the shared session directory is unreachable or the confirmation exceeds the edge timeout). Conversely, fall back to stateless handling only when the tool is declared idempotent, or when cluster-wide absence is confirmed. The non-obvious trap is treating "shared directory timeout" as "session absent" — a timeout is *unknown*, not *absent*, and unknown must map to rejection, not fallback. This is the single most common misconfiguration: the fallback path is wired to the *negative* result of the lookup, when it should be wired only to the *confirmed-negative* result. ## Operational Effect After correcting the fallback trigger to require confirmed cluster-wide absence, the observed effect was a shift from silent data loss to explicit, fast failures: affected requests returned `409 Session Reassigned` within the edge timeout, and clients that honored the retry contract re-established sessions with no orphaned mutations. The 3% rehash cohort experienced one visible error per session instead of a delayed, ambiguous `resources/read` failure 40 seconds later. The cost is a small increase in client-visible errors during failover; the benefit is that every such error is *truthful* — it says the session is gone rather than pretending a new one is the old one. The invariant to encode: a session-bearing request must never be answered by an instance that cannot prove it owns the session or that the session is provably gone. ```python class SessionAffinityViolation(RuntimeError): """Raised when a session-bearing request reaches an instance that cannot prove ownership or cluster-wide absence of the session.""" pass IDEMPOTENT_TOOLS = frozenset({"tools/list", "resources/list", "ping"}) def handle_request(session_id, method, shared_dir, deadline_ok): if session_id is None: return dispatch_stateless(method) if local_session_exists(session_id): return dispatch_with_session(session_id, method) # Local absence alone is NOT sufficient to fall back. if method in IDEMPOTENT_TOOLS: return dispatch_stateless(method) if not deadline_ok: # Directory unreachable within deadline => UNKNOWN, not ABSENT. raise SessionAffinityViolation( f"cannot confirm cluster-wide absence of session {session_id!r} " f"for non-idempotent method {method!r} within deadline" ) if shared_dir.session_exists(session_id): # Another instance owns it; do not silently serve on a fresh session. raise SessionAffinityViolation( f"session {session_id!r} owned by another instance; " f"request must be re-pinned or rejected, not downgraded" ) # Cluster-wide absence confirmed: safe to treat as a new session. return dispatch_stateless(method) ```