Stateless or session-based: a real MCP failure on Standby and the one-line fix

How our MCP server dropped one call in four when Apify Standby spread requests across runs, what the Session not found error actually means, and the single line that fixed it.

We run a small MCP server behind the scenes at Data Signals Lab. It exposes our disclosure tools to AI agents. An agent can ask for the latest congressional trade filings, pull a 13F snapshot, or check a Form D record. The underlying data is public and free. Congressional trade reports come from the House Clerk's financial disclosure portal. Fund holdings come from SEC EDGAR. Our server just wraps that data in tools an agent can call.

For two weeks, that server failed in a way that looked random. Roughly one call in four came back with a 404 and a short message: Session not found. The other three calls worked fine. Same client. Same tool. Same input. This post explains what happened, why the error message is misleading if you read it casually, and the one-line change that fixed it. If you run an MCP server on any platform that scales horizontally, this failure mode is waiting for you.

The setup

Our server uses the official MCP TypeScript SDK with the Streamable HTTP transport. The SDK is open source and free, as are the Python and other official SDKs. We host the server as an Apify Actor in Standby mode. Standby keeps the Actor running as a web server. Requests come in over HTTPS and the platform routes them to a live run of the Actor.

The key word is "a" run. Standby is allowed to keep more than one run alive. When load rises, the platform starts additional runs and spreads incoming requests across them. This is normal horizontal scaling. It is also exactly what you want, right up until your server quietly assumes it is the only copy of itself in the world.

Ours did.

The symptom

The failure pattern was strange at first glance. During quiet hours, everything worked. During our busy ingestion windows, when agents hammer the tools to refresh signals, calls started failing with Session not found. Averaged over a full day, about one call in four was lost. The failures clustered in the exact windows where the data mattered most.

That clustering was the tell. Load was the trigger. Under load, Standby scaled from one run to two. Once two runs were alive, the routing layer split traffic between them. And our sessions lived in the memory of a single run.

What "Session not found" actually means

The Streamable HTTP transport in MCP has an optional session mechanism. The specification describes it plainly. When a client sends its initialize request, the server may respond with an Mcp-Session-Id header. From then on, the client must attach that header to every request. If the server receives a session ID it does not recognize, it must respond with HTTP 404. The client is then expected to start over with a fresh initialize request.

So Session not found does not mean the network dropped something. It does not mean the client sent a malformed request. It means the server looked up the session ID in whatever store it uses and found nothing. In the TypeScript SDK's stateful mode, that store is typically a plain in-memory map. You create a transport with a session ID generator, the SDK mints an ID, and you keep the transport in a map keyed by that ID.

Now put two runs behind one hostname. The client initializes. The request lands on run A. Run A mints session abc123 and stores the transport in its own memory. The client makes its next tool call with Mcp-Session-Id: abc123. The router sends that request to run B. Run B checks its map. The map is empty. Run B answers 404, Session not found.

Nothing is broken in any single component. The SDK behaves exactly as documented. Standby behaves exactly as documented. The router behaves exactly as documented. The bug lives in the gap between them: in-memory session state plus stateless request routing. During our busy windows, with two runs sharing traffic about evenly, close to half of session-bound calls landed on the wrong run. Spread across the whole day, that averaged out to the one-in-four loss rate we saw in the logs.

The one-line fix

The MCP Streamable HTTP transport does not require sessions. Sessions are opt-in. In the TypeScript SDK, the opt-in is the sessionIdGenerator option. Our original code looked like this:

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID()
})

The fix:

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined
})

That is the whole change. With the generator set to undefined, the server runs in stateless mode. It never issues an Mcp-Session-Id header. The client never sends one back. Every request is self-contained. Any run can answer any request, because no run holds anything the others lack. In stateless mode you construct a fresh server and transport per request, handle the request, and tear it down. The failure rate went to zero the hour we deployed it.

What you give up, and when sessions are worth it

Stateless mode is not free of trade-offs, so be honest with yourself about what you need.

Sessions buy you three things. The server can push notifications to a specific client between requests. The server can hold per-client context, such as subscriptions or progress state, without the client resending it. And the transport supports resumability, so a dropped stream can pick up where it left off.

If your tools are read-only lookups, as ours are, you need none of that. A tool call arrives with everything required to answer it. The response closes the loop. Stateless is the correct default for this shape of server, and it happens to be the only shape that scales horizontally without extra work.

If you genuinely need sessions, you have three honest options. Pin each session to one run with sticky routing, if your platform supports it. Move session state out of process memory into a shared store such as Redis, and make every run read from it. Or cap the server at a single run and accept the throughput ceiling. All three are more work than one line. That is why it pays to ask whether you need sessions at all before you turn them on.

One more free alternative deserves a mention. If your MCP server only serves agents on the same machine, skip HTTP entirely and use the stdio transport. It costs nothing, has no sessions to lose, and removes the network from the failure surface. We kept HTTP because our agents run in different places, but for local development stdio is the simpler tool.

Why a dropped call actually matters here

For most APIs, a 25 percent failure rate with client retries is an annoyance. For disclosure data, timing is the product. Congressional trades already reach the public with a lag, because members file under the STOCK Act's 45-day window, which we cover in detail in our piece on the 45-day rule. Fund holdings arrive even staler, since 13F filings carry their own quarterly reporting lag, explained in our 13F deadlines guide. When the raw data is already days or weeks old on arrival, your pipeline cannot afford to add its own delays by silently dropping the calls that fetch it.

A failed tool call during an ingestion window meant a signal scored late or scored on stale inputs. The agents retried, and retries under load made the load worse, which kept the second Standby run alive longer, which sustained the failure. Fixing the one line broke that loop too.

The lesson is small and portable. When a distributed system returns "not found" for something you know you created, ask which copy of the server you are talking to. The error is telling you where the state is not, and that is often more informative than where it is. Nothing in this post is investment advice.

If you want to see what those recovered ingestion windows feed, the output is public. Our Congress Stock Trades report scores every new House disclosure as it lands, using the same tools this server exposes. You can read the current report at Congress Stock Trades and judge the signals for yourself.


Want the signal instead of the raw filings? Get a free report preview. Prefer the tool to the write-up? Browse all data feeds or connect the free MCP server.