DEV Community

Jack M
Jack M

Posted on

MCP Session Architecture: Scale Agent Integrations Without Sticky Servers

AI agents rarely fail because the demo was bad. They fail when the same workflow must run for many users, across many tools, behind real load balancers, with logs, retries, auth, and cost limits. That is where a small MCP server that worked on one laptop can turn into a production bottleneck.

The important shift: MCP is moving from “one client talks to one remembered server” toward a more web-native design. If you build agent integrations, this is your chance to avoid sticky sessions, fragile in-memory state, and tool calls that disappear when a container restarts.

This guide shows a practical MCP session architecture for builders who want agent workflows that scale without becoming ungovernable.

Why MCP Session Design Suddenly Matters

Model Context Protocol, or MCP, gives AI agents a standard way to reach tools, files, databases, APIs, and internal systems. Instead of every team inventing a custom connector pattern, MCP gives clients and servers a shared protocol.

That standardization is useful, but it also exposes a scaling problem.

A local MCP server can keep state in memory. A production MCP server usually cannot. Once you add multiple instances, regional routing, autoscaling, container restarts, and long-running agent workflows, you need a clear answer to a simple question:

When the next tool call arrives, who remembers what happened before?

Recent industry discussion around MCP has focused on session IDs becoming easier to operate at scale. The practical takeaway for builders is not “sessions are gone.” It is this:

Do not make one process the only place where workflow truth lives.

The Common MCP Scaling Trap

A basic MCP setup often looks like this:

Agent client -> MCP server process -> Internal tool/API
Enter fullscreen mode Exit fullscreen mode

That is fine for local development. The server can store session metadata in memory:

const sessions = new Map();

function createSession(clientId) {
  const sessionId = crypto.randomUUID();
  sessions.set(sessionId, {
    clientId,
    createdAt: Date.now(),
    toolBudget: 100,
    lastToolCall: null
  });
  return sessionId;
}
Enter fullscreen mode Exit fullscreen mode

This breaks down when you deploy more than one server instance:

                +----------------+
Agent client -> | Load balancer  |
                +-------+--------+
                        |
          +-------------+-------------+
          |             |             |
       MCP A         MCP B         MCP C
   has session     no session     no session
Enter fullscreen mode Exit fullscreen mode

If the first request lands on MCP A and the next one lands on MCP B, in-memory session state disappears. You can force sticky sessions, but that creates its own problems:

  • uneven load distribution
  • harder autoscaling
  • messy failover
  • poor regional routing
  • fragile blue/green deploys
  • hidden coupling between connection and state

Sticky sessions are sometimes acceptable as a temporary bridge. They should not be the foundation of your agent platform.

A Better Mental Model: Session State Is Data, Not Process Memory

A production MCP session should be treated like a normal web session with stricter requirements.

The server process may execute a tool call, but durable workflow truth should live outside that process.

Agent client
   |
   v
MCP gateway / load balancer
   |
   +--> MCP server instance
   |       |
   |       +--> Redis/session store
   |       +--> Postgres/audit log
   |       +--> Tool APIs
   |
   +--> metrics, traces, policy checks
Enter fullscreen mode Exit fullscreen mode

This design gives every instance access to the same minimum state:

  • session identity
  • tenant or workspace ID
  • authenticated user
  • allowed tools
  • budget limits
  • stream cursor or event ID
  • latest workflow checkpoint
  • cancellation status
  • audit trace ID

The goal is not to store every token and every thought. The goal is to store enough truth to route, resume, deny, retry, audit, and recover.

Recommended Production Architecture

1. Put an MCP gateway in front

Do not expose every tool server directly to every agent client. Use a gateway or edge layer that handles common concerns:

  • authentication
  • tenant lookup
  • rate limits
  • request size limits
  • schema validation
  • Origin checks for HTTP transport
  • session lookup
  • trace ID creation
  • routing to the right MCP service

The gateway can be a dedicated service, an API gateway, or a small reverse proxy plus middleware. The point is to centralize rules that should not be duplicated across every tool server.

2. Prefer Streamable HTTP for hosted servers

The MCP transport docs define stdio for local subprocess communication and Streamable HTTP for independent servers. For hosted, multi-user deployments, Streamable HTTP is usually the better default.

Why it helps:

  • each client message is an HTTP POST
  • servers can return JSON or stream with SSE when needed
  • standard load balancers understand the traffic shape
  • the server can run as an independent service
  • clients can reconnect and resume when supported

A simple hosted flow:

POST /mcp
Accept: application/json, text/event-stream
Mcp-Session-Id: sess_123

{ "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {...} }
Enter fullscreen mode Exit fullscreen mode

The exact headers and protocol version depend on your MCP implementation, but the design principle is stable: make every request self-describing enough that any healthy instance can handle it.

3. Store session state outside the MCP server

Use Redis, Postgres, DynamoDB, or another low-latency store depending on your needs.

Redis is often useful for hot state:

type McpSession = {
  sessionId: string;
  tenantId: string;
  userId: string;
  allowedTools: string[];
  budgetRemaining: number;
  createdAt: number;
  expiresAt: number;
  lastEventId?: string;
  status: "active" | "cancelled" | "expired";
};
Enter fullscreen mode Exit fullscreen mode

Postgres is better for durable audit history:

create table mcp_tool_calls (
  id uuid primary key,
  session_id text not null,
  tenant_id text not null,
  user_id text not null,
  tool_name text not null,
  request_hash text not null,
  status text not null,
  cost_units integer not null default 0,
  created_at timestamptz not null default now()
);
Enter fullscreen mode Exit fullscreen mode

A useful split:

State type Good home Why
active session metadata Redis fast reads, TTL support
audit trail Postgres durable, queryable
large tool outputs object storage cheaper, avoids huge rows
semantic memory vector DB/search index retrieval-specific
policy config database/config service versioned, reviewable

4. Make tool calls idempotent

Agents retry. Networks drop. Streams break. Users refresh tabs. Providers time out.

If a tool call can create, delete, charge, email, or mutate customer data, it needs an idempotency key.

function makeToolCallKey(input: {
  sessionId: string;
  toolName: string;
  requestId: string;
}) {
  return `${input.sessionId}:${input.toolName}:${input.requestId}`;
}
Enter fullscreen mode Exit fullscreen mode

Before executing the tool, check whether the same operation already completed:

async function runToolCall(call) {
  const key = makeToolCallKey(call);
  const existing = await db.toolResult.findByIdempotencyKey(key);

  if (existing) return existing.result;

  await db.toolResult.insertPending(key, call);

  try {
    const result = await executeTool(call);
    await db.toolResult.markSucceeded(key, result);
    return result;
  } catch (error) {
    await db.toolResult.markFailed(key, safeError(error));
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

This one pattern prevents many duplicate side effects.

5. Design for stream recovery

If you use SSE or long-running responses, assume disconnects will happen.

Track event IDs or checkpoints:

type StreamCheckpoint = {
  sessionId: string;
  requestId: string;
  lastEventId: string;
  lastStep: "planned" | "tool_started" | "tool_done" | "final_answer";
  updatedAt: number;
};
Enter fullscreen mode Exit fullscreen mode

When the client reconnects, the server should be able to answer:

  • Did the tool call finish?
  • Which events were already delivered?
  • Can the stream continue?
  • Should the client poll the final result instead?
  • Was the request cancelled?

You do not need a perfect replay system on day one. You do need a clear recovery contract.

Security Requirements You Should Not Skip

MCP servers connect agents to real systems. Treat them like production API infrastructure, not helper scripts.

At minimum:

  • validate Origin for Streamable HTTP connections
  • require authentication for hosted servers
  • bind local servers to localhost when possible
  • validate JSON-RPC method names and schemas
  • deny unknown tools by default
  • scope credentials per tenant and user
  • redact secrets before logging
  • attach trace IDs to every tool call
  • set request body limits
  • enforce timeouts

A small policy check can block a large class of mistakes:

function authorizeToolCall(session: McpSession, toolName: string) {
  if (session.status !== "active") {
    throw new Error("Session is not active");
  }

  if (!session.allowedTools.includes(toolName)) {
    throw new Error(`Tool not allowed: ${toolName}`);
  }

  if (session.budgetRemaining <= 0) {
    throw new Error("Session budget exceeded");
  }
}
Enter fullscreen mode Exit fullscreen mode

Prompts are not security boundaries. Runtime checks are.

What to Keep Stateless

A stateless MCP server does not mean “no state exists.” It means the process does not own state that another process cannot recover.

Good candidates for stateless handling:

  • request parsing
  • schema validation
  • auth token verification
  • routing decisions based on external config
  • tool execution workers
  • response formatting
  • metrics emission

Bad candidates for process-only memory:

  • tenant permissions
  • remaining budget
  • approval status
  • long-running workflow progress
  • idempotency records
  • audit logs
  • stream recovery cursor
  • cancellation state

If losing one container would corrupt the workflow, that data should not live only inside that container.

Load Balancer Strategy

Start simple:

  1. One public MCP endpoint.
  2. Health checks for each server instance.
  3. Short request timeouts for normal JSON responses.
  4. Longer but bounded timeouts for streams.
  5. External session store.
  6. No sticky sessions unless a specific legacy transport requires it.

For streaming workloads, test your actual infrastructure. Some proxies buffer responses. Some enforce idle timeouts. Some behave differently for HTTP/2, SSE, and chunked transfer.

Run these tests before launch:

  • client disconnect during a tool call
  • server restart during a stream
  • duplicate POST retry
  • expired session ID
  • cancelled workflow
  • tool timeout
  • bad JSON-RPC body
  • load balancer sends next request to a different instance

If those tests pass, your agent platform is already ahead of many prototypes.

Cost and Reliability Controls

Session architecture is also cost architecture.

A session should carry enough metadata to control spend:

type Budget = {
  maxToolCalls: number;
  maxModelCalls: number;
  maxRuntimeMs: number;
  maxCostUnits: number;
};
Enter fullscreen mode Exit fullscreen mode

Enforce budget at the gateway and inside tool execution. Do not wait for the final answer to discover that the agent loop ran 80 tool calls.

Useful metrics:

  • tool calls per session
  • failed tool calls per tool
  • retry count per session
  • average session duration
  • stream disconnect rate
  • cost per accepted outcome
  • sessions cancelled by policy
  • sessions resumed successfully

The best metric is not “tokens used.” It is useful work completed per unit of cost.

A Practical Build Checklist

Use this checklist before exposing an MCP server to real users:

  • [ ] Hosted MCP endpoint uses authenticated Streamable HTTP where appropriate.
  • [ ] Session state is stored outside process memory.
  • [ ] Every tool call has a trace ID.
  • [ ] Risky tool calls use idempotency keys.
  • [ ] Unknown tools are denied by default.
  • [ ] Session budget is checked before each tool call.
  • [ ] Tool schemas are validated at runtime.
  • [ ] Stream disconnects have a recovery path.
  • [ ] Audit logs record who called what, when, and why.
  • [ ] Load balancing works without sticky sessions.
  • [ ] Container restarts do not lose workflow truth.
  • [ ] Secrets are never written to logs.

Final Takeaway

MCP makes agent integrations easier to standardize. It does not remove the need for production architecture.

If your MCP server depends on sticky sessions and in-memory workflow state, it may work in a demo and fail under real usage. A better design puts session truth in shared stores, keeps tool calls idempotent, treats streams as recoverable, and lets load balancers do their job.

The simplest rule is the safest one:

Any healthy MCP server instance should be able to continue the next step of the workflow with only the request, the authenticated identity, and shared session state.

Build that way, and your agents can scale without becoming fragile.

FAQ

What is MCP session architecture?

MCP session architecture is the way an MCP deployment tracks client sessions, tool calls, stream state, permissions, retries, and audit data across server instances. In production, this usually means storing important state outside the MCP process so any healthy instance can continue the workflow.

Do MCP servers need sticky sessions?

Not always. Sticky sessions can help with older or highly stateful designs, but they make scaling and failover harder. A stronger pattern is to keep session data in Redis, Postgres, or another shared store so requests can move across instances safely.

What is a stateless MCP server?

A stateless MCP server is a server where the process does not hold unique workflow truth in memory. It can still read and write state, but that state lives in external systems such as a session store, database, queue, or audit log.

Is Streamable HTTP better than stdio for production MCP deployments?

For hosted multi-user systems, Streamable HTTP is usually a better fit because it works with standard HTTP infrastructure, independent server processes, authentication layers, and load balancers. Stdio remains useful for local tools launched as subprocesses.

How should MCP servers handle long-running tool calls?

Long-running tool calls should use timeouts, idempotency keys, checkpoints, cancellation handling, and stream recovery. The client should be able to reconnect or poll for the final result without causing duplicate side effects.

Where should MCP audit logs be stored?

Store audit logs in a durable database or logging system, not only in process logs. At minimum, record session ID, tenant ID, user ID, tool name, request hash, status, time, cost, and trace ID.

Top comments (4)

Collapse
 
alexshev profile image
Alex Shev

Session design is where MCP stops being a demo and becomes infrastructure. If state, identity, and tool permissions are tied too tightly to one server process, scaling the agent layer gets fragile fast.

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

Externalizing session state removes stickiness, but it also turns local state changes into distributed concurrency. I would make the session record versioned and update budgets, cancellation, checkpoints, and tool status with compare-and-set semantics. Otherwise two healthy instances can both read “10 units remaining,” execute, and each write back 9. Long-running work also needs a lease plus fencing token so an expired worker cannot commit after a replacement has resumed the operation.

There is a security boundary too: the session ID should be treated as a locator, not proof of tenant or user identity. Re-authenticate the request, bind the stored session to that principal, use short expiry/rotation, and check authorization revocation on every sensitive call. For recovery, keep a durable operation journal with states such as accepted, started, side_effect_committed, result_recorded, and delivered. Idempotency handles duplicate attempts; the journal handles the harder ambiguity where a tool succeeded but the worker died before recording the result. Stateless compute works when state transitions—not just state storage—have explicit consistency rules.

Collapse
 
neelagiri65 profile image
Neelagiri65

sticky sessions are the thing that quietly caps how far you can scale any of this.

the moment session state lives on one server instance you are one restart away from dropping every agent connected to it.

did you end up externalising the session store or is it still in memory just spread differently ?

Collapse
 
jianqiang_lou_46699ce6ff0 profile image
jianqiang lou

This matches what I’ve seen too. For utility pages, the boring details matter a lot: labels, defaults, edge cases, and whether the result is easy to understand without reading a wall of text.