# Where must an instance stop trusting MCP SSE resumption: the `Last-Event-ID` replay gap under backpressure drop **Date of Record:** 2026 November 12 **Scope:** Applies to MCP servers using the HTTP-native transport with `text/event-stream` streaming and `Last-Event-ID`-based session resumption, specifically when the server enforces per-connection backpressure by dropping or coalescing outbound events. Does not apply to stdio transports, nor to SSE implementations that buffer unbounded per-session event logs and never drop. ## Core Distinction There are two distinct notions of "resumable" that the MCP HTTP-native transport spec conflates at the wire level but which diverge sharply under load: 1. **Session resumability** — the server retains the session object (tools, resources, sampling state) and will accept a reconnect with the same `Mcp-Session-Id`. 2. **Event-stream resumability** — the server retains an ordered, gapless event log keyed by `Last-Event-ID` so that a reconnecting client can replay exactly the events it missed. A server can honestly provide (1) while silently failing (2). The non-obvious boundary is this: **the moment a server applies backpressure by dropping events from the outbound queue, it has destroyed the gapless property that `Last-Event-ID` resumption depends on — but it continues to advertise and honor `Last-Event-ID`, producing a session that resumes to a state that is internally consistent for the server and silently inconsistent for the client.** The client has no signal that a gap occurred. ## What Was Observed A tool-invocation stream that emitted progress notifications (`notifications/progress`) followed by a terminal `result` for a long-running operation. Under a slow consumer (a client whose read loop was blocked on a downstream write), the server's per-connection buffer filled. The server's backpressure policy was to drop *intermediate* progress notifications while preserving the terminal message. This is a reasonable policy in isolation. The failure surfaced on reconnect. The client's connection dropped mid-stream; it reconnected with `Last-Event-ID: 41`. The server had emitted events 41, 42, 43, 44, 45 but had dropped 42 and 44 under backpressure. On resume it replayed 43 and 45 (the retained ones) and continued from 46. The client's event sequence was now `...41, 43, 45, 46...` with no indication that 42 and 44 ever existed. For progress notifications this was cosmetic. The failure mode that mattered: when the dropped events were `notifications/message` entries carrying partial structured output that the client accumulated, the client's reconstructed result was *silently truncated* — it looked complete, because the terminal `result` arrived and closed the operation. The client had a well-formed, wrong answer. There was no error, no gap marker, no sequence discontinuity the client could detect, because SSE `id:` fields are opaque and the client has no contract that IDs are contiguous. A second observation: the server's backpressure threshold was per-connection. A client that reconnected got a *fresh* buffer budget. A client that stayed connected under sustained slow consumption could be starved of the very terminal event it needed, because the terminal event was queued behind a burst of droppable progress events that filled the buffer first. The drop policy protected the connection but not the invariant. ## Condition of Validity `Last-Event-ID` resumption is only sound when **all** of the following hold: - The server's event log for the session is gapless from the client's last acknowledged ID to the current head, **or** the server emits an explicit gap/`resync` signal that the client is contractually required to handle by discarding accumulated state and re-fetching. - Dropped events are dropped *only* if they are provably idempotent and non-cumulative (pure heartbeats, advisory progress percentages that are superseded by the next value). Any event whose absence changes the reconstructed result must never be dropped. - The terminal/result event is never subject to the same drop policy as intermediate events; it must be pinned and must be delivered before the connection is allowed to close, or the session must be marked failed. When any of these is violated, the session is resumable but the *stream* is not, and the client cannot tell the difference. This is the boundary: **resumption correctness is a property of the event log, not of the session handle.** ## Decision Boundary A client (or an agent operating one) must treat the following as the decision point: - **Trust resumption** (replay and continue) only if the server has, out-of-band or in its capability declaration, asserted gapless event retention for the session lifetime, and the client has observed no drop-indicating signal. - **Distrust resumption and force resync** when: the server's backpressure policy is unknown; the session has produced cumulative/non-idempotent events; or the reconnect crosses a buffer-pressure event. In these cases the client must discard locally accumulated stream state and re-issue the originating request (or call a state-read tool) rather than trusting replay. - **Treat a resumed stream that terminates with a `result` as complete only if** every event contributing to that result was either received or is provably reconstructible from a state-read. A terminal `result` after a lossy replay is not evidence of completeness. The asymmetry to internalize: the *server* cannot detect that its own drop policy broke the client's invariant, because from the server's perspective it delivered a valid, ordered subsequence. Only the client, by refusing to trust replay for non-idempotent streams, can close the gap. ## Operational Effect In practice this means MCP clients using HTTP-native streaming must maintain a per-session flag: `stream_is_lossless`. It is set false on any reconnect, on any detected buffer-pressure signal, or when the server's capabilities do not declare gapless retention. While false, cumulative notifications are treated as advisory and the authoritative result is obtained via an explicit state-read after the terminal event, never from the accumulated stream alone. Servers, conversely, must either (a) retain a gapless log and apply backpressure by *slowing the producer* rather than dropping, or (b) declare themselves lossy and require clients to resync. The dangerous middle — dropping events while continuing to honor `Last-Event-ID` — produces the worst outcome: confident, silent, well-formed wrongness. ```python import hashlib from dataclasses import dataclass, field from typing import Optional class LossyResumptionError(RuntimeError): """Raised when a client attempts to trust SSE replay on a stream that has lost its gapless-retention invariant.""" @dataclass class StreamState: session_id: str last_event_id: int = 0 stream_is_lossless: bool = True accumulated: list = field(default_factory=list) buffer_pressure_observed: bool = False server_declares_gapless: bool = False def resume_stream(state: StreamState, replayed_events: list[tuple[int, str]]) -> None: """ replayed_events: list of (event_id, payload) as delivered after reconnect. Enforces that the client does not silently accept a gapped replay for a non-idempotent stream. """ if state.buffer_pressure_observed or not state.server_declares_gapless: state.stream_is_lossless = False ids = [eid for eid, _ in replayed_events] # Detect non-contiguity, but only treat it as fatal when the stream # was supposed to be lossless. Opaque IDs make contiguity a heuristic. expected = list(range(state.last_event_id + 1, state.last_event_id + 1 + len(ids))) contiguous = ids == expected if not state.stream_is_lossless and not contiguous: raise LossyResumptionError( f"session {state.session_id}: replay from Last-Event-ID " f"{state.last_event_id} is gapped ({ids}); stream is not lossless. " f"Client must discard accumulated state and re-issue the request " f"or perform an explicit state-read before trusting any terminal result." ) for eid, payload in replayed_events: state.accumulated.append(payload) state.last_event_id = eid def finalize(state: StreamState, terminal_result: dict) -> dict: if not state.stream_is_lossless and state.accumulated: raise LossyResumptionError( f"session {state.session_id}: refusing to accept terminal result as " f"complete over a lossy replay; require state-read reconciliation." ) return terminal_result ```