MCP Goes Stateless: What Changes for Agent Builders

Rows of network servers with cables in a data center, illustrating stateless MCP server deployment

You spent a weekend wiring an MCP server into your agent, got it working locally, then watched it fall over the moment you put it behind a load balancer. Session IDs pinned to one process, sticky routing headaches, weird 400s when a client reconnects. If that sounds familiar, the protocol update you've been waiting for just landed.

The Model Context Protocol — the thing that lets Claude, ChatGPT, Cursor and a growing pile of agent frameworks talk to your tools — is moving toward a stateless session model on the server side. It's a small spec change with outsized operational consequences, especially if you're a solo builder or a small team running production agents without an SRE on staff.

What actually changed in the MCP session model

The short version: MCP servers are no longer required to hold session state in memory between requests. Under the older transport model, when a client connected it got a session ID back in a header, and every subsequent request had to hit the same server process that issued it. That's fine on your laptop. It's painful in production, because it forces sticky sessions, in-memory maps, and a whole class of "why did this agent forget who it was" bugs.

The new approach treats sessions more like the rest of the web already does: the client sends whatever it needs to identify itself on every request, and the server is free to be completely stateless. If you want persistence, you put it in a database or a cache — the transport doesn't force you into a specific architecture anymore.

Practically, this means:

  • Any MCP request can be handled by any replica of your server.
  • You can put your MCP server behind a standard HTTP load balancer with no sticky-session config.
  • Cold starts on serverless platforms (Lambda, Cloud Run, Vercel Functions, Fly Machines) stop being a correctness problem — they're just a latency question.
  • Horizontal scaling is boring again, which is exactly what you want.

If you've deployed real HTTP APIs before, none of this is surprising. That's the point. MCP is starting to look like the rest of your stack.

Why stateful sessions were a production tax

To understand why this matters, it helps to see the concrete failure modes the old model produced. I've hit all of these on client projects in the last year:

Sticky routing headaches. You deploy two replicas of your MCP server behind an ALB. Client A opens a session, gets routed to replica 1, and everything works. You push a new deploy, replica 1 gets replaced, the client's next request goes to replica 2, and the server returns a session-not-found error. Now you're writing custom logic to either pin the client via a cookie or fall back to re-initialization on every 4xx.

Memory leaks that look like bugs. Session maps in memory grow forever if you don't wire up a TTL. In one audit I did last spring, a small MCP server was leaking about 40MB per hour because the team never got around to expiring idle sessions. It ran fine for a week, then OOMed at 3AM on a Saturday.

Serverless was basically off the table. If your session state lives in a single Node process and that process can be recycled between requests, sessions vanish. Teams either pinned to always-on VMs or wrote elaborate session-rehydration layers that mostly negated the point of using serverless.

Rolling deploys hurt. Every deploy killed in-flight sessions. For an agent that's mid-task, that means retries, partial state, and sometimes duplicate side effects (an email sent twice, an invoice generated twice) if your tools weren't idempotent.

Stateless mode doesn't eliminate any of the underlying concerns — you still need to think about idempotency, auth, and persistence — but it moves them into layers you already know how to handle.

What "stateless" actually means (and doesn't)

There's a small trap here worth calling out, because I've seen people misread the spec update on Twitter. Stateless doesn't mean your agent forgets things. It means the transport doesn't require the server process to remember them.

Concretely:

Layer Where state lives (before) Where state lives (after)
MCP transport Server memory, keyed by session ID Nowhere required — client sends what's needed
Tool state (e.g. "which repo am I in") Often mixed into session memory Explicit: passed in tool args, or stored in your DB
Conversation history The MCP client (agent host) Same — unchanged
Auth Session-scoped tokens Standard bearer tokens per request

If your tools need memory — a running task, a long computation, an upload in progress — you put that memory in Redis, Postgres, S3, or wherever it belongs. The MCP layer stops pretending to be your state store.

This is genuinely how most well-designed HTTP APIs work. Stripe doesn't hold your session in memory between requests to /v1/charges. Neither does GitHub's REST API. MCP is just catching up.

A minimal stateless MCP server, end to end

Here's what a stateless MCP tool server looks like in Python once you strip the ceremony. This uses a Streamable HTTP transport and treats each request as independent.

from mcp.server.fastmcp import FastMCP
import os, httpx

mcp = FastMCP("invoice-tools", stateless_http=True)

@mcp.tool()
async def get_invoice(invoice_id: str) -> dict:
    """Fetch an invoice by ID from the billing system."""
    api_key = os.environ["BILLING_API_KEY"]
    async with httpx.AsyncClient() as client:
        r = await client.get(
            f"https://billing.internal/invoices/{invoice_id}",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=10.0,
        )
        r.raise_for_status()
        return r.json()

@mcp.tool()
async def mark_paid(invoice_id: str, idempotency_key: str) -> dict:
    """Mark an invoice paid. Idempotent via idempotency_key."""
    api_key = os.environ["BILLING_API_KEY"]
    async with httpx.AsyncClient() as client:
        r = await client.post(
            f"https://billing.internal/invoices/{invoice_id}/pay",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Idempotency-Key": idempotency_key,
            },
            timeout=15.0,
        )
        r.raise_for_status()
        return r.json()

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

A few things to notice:

  • No session dict. No sessions[session_id] = ... anywhere.
  • Every tool takes exactly what it needs as arguments. mark_paid requires an idempotency_key from the caller — because the client (the agent) is the one that should decide when two requests are "the same request."
  • Auth is a plain bearer token from the environment. Rotate it in your secrets manager, done.
  • This container can be deployed to Cloud Run, Fly, ECS, or a Kubernetes Deployment with replicas: 5 and no other config.

Compare that to what the same server would need with sticky sessions: a session store, a session-expiry job, sticky routing config on the load balancer, and a bunch of defensive code for "what if the session vanished between requests."

Deployment: what to actually put in front of it

Once your server is stateless, deployment collapses to something almost boring. Here's a Docker + Fly configuration I've used for small production MCP servers:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
EXPOSE 8000
CMD ["python", "server.py"]
# fly.toml
app = "invoice-mcp"
primary_region = "iad"

[http_service]
  internal_port = 8000
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 0

[[vm]]
  size = "shared-cpu-1x"
  memory = "512mb"

auto_stop_machines and min_machines_running = 0 are the important lines. Because the server is stateless, you can let the machine sleep when idle and cold-start it on the first request. For a small internal tool that gets used a few times an hour, that's the difference between paying for a VM 24/7 and paying for compute you actually use.

If your agents are latency-sensitive, keep min_machines_running = 1. But the point is you get to choose per-workload instead of being forced into "always on" by the transport.

The things stateless does NOT fix

Before you rip out your session store, be honest about what stays hard:

Idempotency is now your job, explicitly. In the old model, some servers half-solved duplicate-request problems by tracking request IDs in session memory. Now you need an idempotency key on every mutating tool call, stored in Redis or a database with a TTL. That's the right design anyway, but it's work.

Long-running tools still need somewhere to live. If a tool takes 90 seconds — say, generating a PDF report — you can't hold the HTTP connection open across a load balancer that closes idle sockets at 60 seconds. The pattern here is: start the job, return a job ID, expose a second tool for polling. Same as any modern job queue.

Auth gets a bit more explicit. No more "we authed once at session start, now all tool calls inherit that." Every request carries its own credential. For OAuth-flavored setups, this usually means a short-lived bearer token that the client refreshes. Not hard, but not free.

Rate limiting shifts. You can no longer count "requests per session" in memory on one node. Use a shared store (Redis with a sliding window) or push rate limits to your gateway (Cloudflare, an ALB with WAF rules, etc.).

None of these are dealbreakers. They're the normal cost of running a real HTTP service, which is what your MCP server now is.

Migration checklist for an existing MCP server

If you already have a stateful MCP server in production, here's the order I'd move in. Don't try to do it all in one PR.

  1. Inventory your session state. Grep for anywhere you write to a session-keyed dict. For each entry, decide: does this belong in the tool arguments, in a shared cache, or in a persistent DB?
  2. Make tools accept explicit arguments. If a tool currently reads session["current_project_id"], change its signature to accept project_id as a parameter. Update the tool description so the agent knows to pass it.
  3. Add idempotency keys to every mutating tool. Store used keys in Redis with a TTL of at least 24 hours. Reject or short-circuit duplicates.
  4. Move persistent state to a real store. Uploads-in-progress go to S3. Job state goes to Postgres or a job queue. Auth tokens live in your secrets manager and refresh on a schedule.
  5. Flip the transport flag. Turn on stateless mode. Run both old and new in parallel behind a feature flag for a week.
  6. Remove sticky routing. Once you're confident, drop the sticky-session cookie config on your load balancer. Test rolling deploys.
  7. Right-size your infra. You can probably run fewer, smaller instances now — or move to serverless entirely for low-traffic servers.

Budget one to three days for a small server (a handful of tools). More if your tools have deep state assumptions baked in.

How BizFlowAI approaches this

We build and run MCP integrations for small businesses as day-to-day work — connecting agents to invoicing systems, CRMs, internal databases, support inboxes, and the assortment of SaaS tools an SMB actually uses. The stateless shift matters to us because it removes a real chunk of the operational overhead that used to make "put an agent in front of your ops workflow" a bigger project than it needed to be.

In practice, that means new client MCP servers ship on serverless infrastructure by default, with idempotency and auth handled properly on day one, and deploys that don't require a maintenance window. If you have a specific workflow you want an agent to handle — invoice chasing, lead qualification, support triage — a discovery call is the fastest way to figure out whether MCP is the right tool for it and what the smallest useful version looks like.

What to build first now that this is easier

If you've been holding off on production MCP work because the operational story was rough, the constraint just got lighter. A few low-risk starter projects that are worth an afternoon:

  • A read-only server over your internal knowledge. Wrap 3-5 lookup endpoints (customer info, order status, inventory) as MCP tools. Point your agent host at it. This alone often replaces a chunk of Slack-based "can you look up X for me" traffic.
  • A one-tool automation with strong idempotency. Pick one repetitive action — sending a follow-up email, creating a Linear ticket, updating a spreadsheet row — and expose it as a single tool with an idempotency key. Let the agent decide when to call it.
  • A polling wrapper around a slow job. If you have a report or export that takes minutes to generate, expose "start" and "check status" as two tools. This teaches you the async pattern you'll need for anything real.

Each of these fits in a single stateless server, deploys to a $5/month container, and gives you a concrete feel for where MCP actually helps versus where a plain script would do. Start there, measure what you save, and let the next server be justified by the numbers from the first.

The protocol getting simpler doesn't mean the design work goes away — it just means less of your time is spent fighting the transport, and more of it is spent on the tools that actually move your business.


Work with BizFlowAI

If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.

Book a free discovery call — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.

More guides like this on the BizFlowAI blog.

Frequently asked questions

What does stateless mode mean in the Model Context Protocol?

Stateless mode in MCP means the server transport is no longer required to keep session state in memory between requests. Clients send whatever identifying information they need on every request, so any replica can handle any call. It does not mean agents lose memory or conversation history — those live in the client or in your own database. It only removes the requirement that requests hit the same server process that issued the session ID.

How do I deploy an MCP server behind a load balancer without sticky sessions?

Use the new stateless HTTP transport (for example, FastMCP with stateless_http=True in Python) so no request depends on server-side session memory. Any tool state should be stored in Redis, Postgres, or another shared backend, and auth should use standard bearer tokens sent on each request. Then you can put the server behind any standard HTTP load balancer with multiple replicas and no sticky routing. Rolling deploys and horizontal scaling then work like any normal HTTP API.

Can I run an MCP server on serverless platforms like Cloud Run or AWS Lambda?

Yes, once the MCP server is stateless, serverless platforms become viable because cold starts no longer break session correctness — they only add latency. Each request is independent, so the process being recycled between calls does not lose important state. You just need to move any persistent tool state into an external store like Redis or a database. Platforms like Cloud Run, Fly Machines, Lambda, and Vercel Functions all work this way.

How do I handle idempotency in a stateless MCP server?

Require the client (the agent) to pass an idempotency_key argument on every mutating tool call. Store that key in Redis or a database with a TTL and check it before executing the action, returning the cached result if the key was seen before. This prevents duplicate side effects like sending an email or charging a card twice when the agent retries. The MCP transport no longer helps with this, so it must be handled explicitly in your tool code.

What problems does stateless MCP not solve?

Stateless MCP does not handle idempotency, long-running tasks, or authentication for you. You still need idempotency keys for mutating operations, a background job system or polling pattern for tools that run longer than a load balancer's idle timeout, and proper token management for auth. It also does not manage conversation history — that stays with the MCP client. It only removes the transport-level requirement to keep session state in server memory.