Stateless Servers, Stateful Payloads: Sessions vs. Continuations, Rust

garyhtou1 pts0 comments

Stateless Servers, Stateful Payloads: Sessions vs Continuations, Measured in Rust

Sign in<br>Subscribe

One Protocol, Three Places to Keep State<br>One rmcp v3 server — the SDK was three days old at measurement. Three state policies behind one trait (sticky memory, Redis store, sealed continuation), plus a held gRPC stream as reference (different transport and codec). 74,550 samples, six replicates; kill matrix, time-travel fork, 22 security tests; every raw CSV committed. Part 1 of the Typed Agents series.<br>Source Code : github.com/zs-dima/sessions-vs-continuations<br>Raw Data : results/ — every measured round, one row each<br>TL;DR (For Skimmers)<br>State on the wire costs nothing measurable at these sizes — inside ±0.27 ms of a memory lookup on loopback, ±0.10 ms at wide-area RTT. A bound, not a win<br>Growth does cost: 250 → 923 bytes of state runs about +120 µs per round by round 10 — three of four control-subtracted contrasts exclude zero, and microbenchmarks predict the magnitude<br>The session store makes 9 Redis trips per four-call dialogue : 4.84 ms per round with the store 1 ms away, 1.40 ms on loopback. Quote both or neither<br>Sticky memory: the dialogue dies with its replica , and 100/100 abandoned dialogues stay held to process exit<br>Legal time travel : an old requestState, re-presented, forked one dialogue into two valid bookings — quoted at 1,388.80 and 804.00. Every protection is a SHOULD ; the only MUST — at-most-once — needs server-side state back<br>MCP Didn't Go Stateless<br>On July 28, 2026 the Model Context Protocol shipped its largest revision since launch, and the retellings mostly agree: MCP went stateless. That is not quite what happened. MCP stopped having an opinion about state. Protocol-level sessions are gone — no more Mcp-Session-Id, no server-held conversation the transport routes back (SEP-2567) — and every server-initiated request became MRTR , Multi Round-Trip Requests (SEP-2322): a tools/call that needs your input returns an InputRequiredResult carrying inputRequests and an opaque requestState; echo it back with your answers and any replica picks the dialogue up.<br>What the spec deliberately does not say is what goes inside that envelope. A pointer into replica memory, a key into Redis, or the whole serialized dialogue: all three conform. The choice left the protocol; it is yours now. So I built all three behind one trait and measured them; this repository is the result. (No overnight sensation: the RC was frozen May 21 and spent ten weeks in public.) The short version — sessions didn't disappear, they moved into the payload — turns out to be measurable.<br>The envelope, in rmcp's sealed form (schematic):<br>requestState = "rs1." + base64url( expiry ‖ state-json ) + "." + base64url( HMAC tag )<br>The official pattern docs cover the anatomy; rmcp bounds the loop at DEFAULT_MRTR_MAX_ROUNDS = 10.<br>A gRPC Engineer's Déjà Vu: Three Places for State<br>The problem is older than MCP: the server asked the client something mid-operation, and the answer must survive a replica change. That in-flight state has only three homes — replica memory behind affinity, an external store, or the wire; cookie-to-JWT, OAuth continuations, and long-polling walked the same doors. This experiment names them:

Arm<br>requestState carries<br>State lives<br>Extra ingredient

A — sticky<br>a mem_ handle<br>replica memory<br>one nginx line: hash $http_x_dialogue_id consistent;

B — session store<br>a sess_ key<br>Redis, under a TTL<br>the Redis service

C — continuation<br>the sealed state itself<br>on the wire<br>a codec key on every replica

D — held stream<br>the task serving the stream<br>reference : different transport and codec

Note on arm D , provenance file, verbatim: "Arm D uses a different transport and codec and is outside the controlled comparison (controlled=false)." It serves as a latency floor and a failure-mode contrast; it is not a fourth competitor.<br>The expected comparison, "MRTR versus gRPC", is a category error: MRTR is a pattern (who holds the dialogue state), gRPC a transport (how bytes travel). They are not even alternatives: MCP has a gRPC transport, contributed by Google. MCP got a gRPC transport — and still replaced server-initiated interactions with continuation-passing at the pattern level.<br>The SEP made this comparison first: its Rationale rejected bidirectional streams (HTTP/2/3 required, no good without long-lived connections, nothing for fault tolerance). Co-written by Mark D. Roth — a gRPC maintainer and member of the gRPC Steering Committee at Google — and sponsored by Caitie McCaffrey: one of the people who knows held streams best chose continuations. The same release removed SSE resumability (SEP-2575): "A broken response stream loses the in-flight request; clients MUST re-issue it as a new request with a new request ID." If arm D's kill-matrix row below looks like a straw man, note that it is the behaviour the spec chose.<br>A, B and C share one binary, one protocol, one codec, one script, and one machine. Exactly two things differ: the StatePolicy...

state three grpc transport replica server

Related Articles