Docs

Reliability and retries

How long an MCP session lasts, what a -32000 means, and the one retry that fixes it

An MCP session is a live connection to a specific Amdahl server process. Most of the time you never think about it. But sessions do end — and when one does, you find out on your next call, not before.

This page is the contract: how long a session lasts, how to recognise the error that means "your session is gone", and the single retry that recovers from it.

How long a session lasts

A session you are using does not expire. There is no maximum lifetime. Keep calling and it stays alive for as long as you need — hours, a full working day, longer.

What ends a session is one of three things:

CauseWhen it happens
IdleNo calls at all for two hours. The session is reclaimed.
CapacityA server holding many sessions makes room for a new one by dropping the quietest.
Server restartA deploy or a restart. Sessions live in process memory, so they all go at once.

Note what capacity does not do: it never picks the session that has been open longest. It picks the one nobody has called in the longest time. An in-use session is not an eviction candidate.

Holding the notification stream open counts as use, so a client that subscribes and then only listens does not go idle while its stream is up.

None of these can warn you first — by the time the session is gone, there is no channel left to tell you on. So the notification is the error on your next call.

-32000: reinitialize, then replay once

When your session is gone, the next call on it fails with JSON-RPC error code -32000 over HTTP 404:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32000,
    "message": "Session not found. Please reinitialize.",
    "data": {
      "reason": "session_not_found",
      "action": "reinitialize_and_replay_once",
      "server_uptime_seconds": 47
    }
  },
  "id": 12
}

Some clients surface the same condition as -32000: Connection closed when the stream drops rather than the request returning. Treat both spellings identically.

The handling rule is one line: open a new session, send the same call again, once.

ts
async function call(request) {
  try {
    return await session.send(request)
  } catch (err) {
    if (!isSessionGone(err)) throw err
    session = await client.initialize() // new session
    return await session.send(request) // replay once — do not loop
  }
}
Do not put -32000 in a backoff loop

Waiting does not help: the session is gone and no amount of delay brings it back. Only a fresh initialize does. If the replay fails on a brand-new session, the problem is the call itself — surface the error rather than retrying again.

Two more things worth building in:

  • Retry the whole call, not the transport. Reinitializing gives you a new session id. Reusing the old one just fails again.
  • -32000 is not fatal. It is the expected steady-state cost of a stateful connection to a deploying service. A client that treats it as an outage will look far less reliable than the service actually is.

Telling our churn from your bug

The data block carries server_uptime_seconds so you can tell what happened without guessing:

If server_uptime_seconds is less than the age of the session you were holding, the server restarted. A process younger than your session never had it.

That comparison is exact, not a heuristic. It answers the only question worth asking at the moment of failure: is this something I did, or something you did?

  • Server younger than your session — a deploy or restart. Nothing on your side to fix. Reinitialize, replay, carry on.
  • Server older than your session — the session hit the two-hour idle budget, was dropped under capacity pressure, or the session id was never valid on this process. If your client had been idle, it is the first. If it was busy and this still happened, tell us — that is a bug on our side and we want the trace.

The signature to recognise

The common shape is a first call after a gap failing, and an immediate identical retry succeeding. That is a session boundary, not flaky infrastructure and not a bad request — the retry proves the call was well-formed. A client with the reinitialize-and-replay rule absorbs it completely and you never see it.

Other failures worth retrying

-32000 is the session-shaped failure. The rest follow ordinary HTTP rules:

StatusRetry?How
429YesBack off before retrying. See Rate limits for the headers to read.
503YesExponential backoff, jittered, with a cap on attempts.
502 / 504YesSame backoff. These come from the gateway in front of us, not from Amdahl itself.
401 / 403No — reauthorizeYour token expired or lost access. Reconnect the connector.
Other 4xxNoThe call is wrong. Retrying sends the same wrong call.

Note the split: -32000 gets exactly one immediate retry with a new session, while 429 and 5xx get backoff on the same session. They are different failures and the same retry policy does not serve both.

If a call is slow enough that you are tempted to raise your client timeout, move it to the async path instead — start a chat and poll — rather than holding a synchronous request open.

Checklist

A client that does these five things will not notice session churn:

  1. Detect -32000 — in both spellings — as its own case, separately from HTTP errors.
  2. On -32000: reinitialize, replay once, then give up.
  3. Never loop on -32000, and never reuse a dead session id.
  4. Back off on 429 and 5xx; do not retry other 4xx.
  5. Log server_uptime_seconds when you see -32000, so a real bug is distinguishable from a deploy after the fact.