Skip to content

Commit 2665a66

Browse files
committed
docs: describe session lifetime and limits in one place
The idle timeout and the session cap were explained piecemeal on three pages. legacy-clients.md gains a "Session lifetime and limits" section with both settings, what counts as in flight, what the client sees and how to turn each off; the options list in run/index.md and the troubleshooting entry now point at it instead of restating it.
1 parent ab91d9c commit 2665a66

4 files changed

Lines changed: 41 additions & 15 deletions

File tree

docs/run/index.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,11 @@ Each transport has its own keyword arguments, all on `run()`:
7070
* `max_request_body_size`: largest accepted request body in bytes. Defaults to 4 MiB; larger requests
7171
receive HTTP 413 before parsing or session creation. Raise it only when legitimate MCP messages
7272
exceed that size.
73-
* `session_idle_timeout`: how long, in seconds, a [legacy](legacy-clients.md) (session-based)
74-
client's session may sit with no request in flight before the server closes it. Defaults to 1800
75-
(30 minutes); `None` keeps sessions until the client deletes them. A client with an open `GET`
76-
stream or a request still being answered is never idle.
77-
* `max_sessions`: how many such sessions one app holds at once. Defaults to 10 000; while that many
78-
are open, a request that would open another gets HTTP 503. `None` removes the limit.
73+
* `session_idle_timeout`: seconds a legacy session may sit with nothing in flight before the
74+
server closes it. Default 1800. `None` disables it. See
75+
[Session lifetime and limits](legacy-clients.md#session-lifetime-and-limits).
76+
* `max_sessions`: how many legacy sessions one process holds at once. Default 10 000. `None`
77+
removes the limit. Covered in the same section.
7978
* `event_store`, `retry_interval`, `transport_security`: resumability and DNS-rebinding protection. They can wait, until you deploy somewhere other than localhost; **[Deploy & scale](deploy.md)** covers `transport_security`.
8079

8180
!!! warning

docs/run/legacy-clients.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,39 @@ On one worker that is invisible. On two, it is the whole problem: a request that
5656
events to a client reconnecting to the *same* session), not a session store. It never makes a
5757
session reachable from another process.
5858

59-
The record is not kept forever. A client that ends its session (`DELETE`) frees it at once;
60-
a session that has had no request in flight for `session_idle_timeout` seconds (default 1800; an
61-
open `GET` stream or a request being answered counts as in flight) is closed, and its next request
62-
gets the same `404` a stray ID gets, so the client has to `initialize` again. Each worker process
63-
holds at most `max_sessions` of them (default 10 000) and answers `503` to a request that would
64-
open one more. Both are `run()` / `streamable_http_app()` options.
59+
## Session lifetime and limits
60+
61+
A legacy session does not live forever, and one process does not hold an unlimited number of
62+
them. Two settings control this. Both are keyword arguments on `run()`, `streamable_http_app()`
63+
and `Server.streamable_http_app()`. Modern (`2026-07-28`) connections and `stateless_http=True`
64+
have no sessions, so neither setting applies to them.
65+
66+
| Setting | Default | What it does | What the client sees | Turn it off |
67+
|---|---|---|---|---|
68+
| `session_idle_timeout` | `1800` (30 min) | Closes a session that has had nothing in flight for that long. | `404 Session not found`. It has to `initialize` again. | `None` |
69+
| `max_sessions` | `10_000` | Refuses to open a session beyond that many. Existing sessions are untouched and nothing is evicted. | `503 Too many open sessions` with JSON-RPC code `-32603`. | `None` |
70+
71+
What counts as "in flight":
72+
73+
* An open `GET` stream. The SDK clients keep one open, so a connected client's session never
74+
expires.
75+
* A request that is still being answered. A tool call that runs longer than the timeout is not
76+
interrupted, and the countdown only starts once it finishes.
77+
* Nothing else. Between requests the clock runs. Any request on the session restarts it,
78+
`ping` included. Once a session has expired, nothing revives it.
79+
80+
A client that ends its session with `DELETE` frees it immediately. So does a client whose
81+
opening request was refused.
82+
83+
```python
84+
mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000)
85+
```
86+
87+
Both events show up in the server log. An expiry is `Session <id> idle timeout` at `INFO`. A
88+
refused open is `Refusing to open a new session: <n> sessions are already open` at `WARNING`.
89+
90+
The limits are per process. With four workers the ceiling is four times `max_sessions`, and each
91+
worker expires its own sessions.
6592

6693
## The one knob: `stateless_http`
6794

docs/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif
246246

247247
## `MCPError: Session not found`
248248

249-
The server does not recognise the `Mcp-Session-Id` your client sent, because the server **restarted** (or you were routed to a different instance), or because the session **expired**: a legacy session with no request in flight for `session_idle_timeout` (30 minutes by default; an open `GET` stream or a request being answered counts as in flight) is closed, as is one the client ended with `DELETE`. Sessions live in that one process's memory.
249+
The server does not recognise the `Mcp-Session-Id` your client sent. Either the server **restarted** (or you were routed to a different instance), or the session **expired** because nothing was in flight for `session_idle_timeout`, which is 30 minutes by default. See [Session lifetime and limits](run/legacy-clients.md#session-lifetime-and-limits). Sessions live in that one process's memory.
250250

251251
There is no server bug to find. The HTTP response is a `404` whose body *is* JSON-RPC, so, unlike the `421` above, the python `Client` shows you this one verbatim:
252252

tests/docs_src/test_legacy_clients.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ async def test_a_legacy_session_is_minted_in_process_and_a_stray_session_id_is_a
7979

8080

8181
def test_legacy_sessions_expire_and_are_capped_by_default() -> None:
82-
"""The cost section: a session record is dropped after 30 idle minutes and each worker process holds at most
83-
10 000 of them, unless `run()` / `streamable_http_app()` say otherwise."""
82+
"""The session lifetime section: a session is closed after 30 idle minutes and each worker process
83+
holds at most 10 000 of them, unless `run()` / `streamable_http_app()` say otherwise."""
8484
server = MCPServer("Bookshop")
8585
server.streamable_http_app()
8686
assert server.session_manager.session_idle_timeout == 30 * 60

0 commit comments

Comments
 (0)