One client connection costs two initialize charges #22

Open
opened 2026-08-28 01:57:12 +00:00 by jlxq0 · 1 comment
Owner

claude-code 2.1.248 posts twice to /mcp without an mcp-session-id, about
30 ms apart. Both are authenticated, both pass is_fresh_mcp_session_request,
both are charged by InitializeLimiter, and both cause rmcp to create a
session. Only the second reaches Service initialized as server.

Measured on the v0.1.4 pod over twelve hours: 16 charged creates produced 5
usable sessions
, six of them orphans that were charged and produced nothing at
all.

00:27:15.001  create new session  f96f37d5…     <- charged, abandoned
00:27:15.033  create new session  f0573a5f…     <- charged, survives
00:27:15.033  Service initialized as server     <- claude-code 2.1.248
00:27:15.039  client initialized

So any capacity stated in connections is half the number in the constant,
which is why v0.1.5 sizes the burst at 32 for eight agents rather than 8.

Charging only the session that survives would double effective capacity
without touching the limit
, which is the fix Alan asked for and I did not
ship. The reason it is an issue rather than a commit: the charge sits in a tower
middleware and the surviving session is established inside rmcp, so moving it to
on_initialized would leave the "half-open but un-handshaken" state ungoverned
— which is the exact thing session.rs documents Mitigation A as existing to
prevent. Doing that at 02:00 with the fleet locked out was not a trade worth
making.

What is unestablished and would decide the design: why the client posts
twice.
Two authenticated POSTs 30 ms apart with different session ids could be
protocol-version negotiation, a probe, or an SSE stream open. Nothing in this
server's log distinguishes them, because the middleware does not record what the
body was. That measurement comes first.

Candidate approaches, none evaluated:

  • Log the JSON-RPC method of a fresh POST so the two are distinguishable, then
    charge only initialize.
  • Collapse charges within a short window per subject, accepting that a burst of
    N then covers up to N connections rather than N/2.
  • Leave it, and keep the burst sized at two per expected connection with the
    ratio written down — which is what v0.1.5 does.
`claude-code 2.1.248` posts twice to `/mcp` without an `mcp-session-id`, about 30 ms apart. Both are authenticated, both pass `is_fresh_mcp_session_request`, both are charged by `InitializeLimiter`, and both cause rmcp to create a session. **Only the second reaches `Service initialized as server`.** Measured on the `v0.1.4` pod over twelve hours: **16 charged creates produced 5 usable sessions**, six of them orphans that were charged and produced nothing at all. 00:27:15.001 create new session f96f37d5… <- charged, abandoned 00:27:15.033 create new session f0573a5f… <- charged, survives 00:27:15.033 Service initialized as server <- claude-code 2.1.248 00:27:15.039 client initialized So any capacity stated in connections is **half** the number in the constant, which is why `v0.1.5` sizes the burst at 32 for eight agents rather than 8. **Charging only the session that survives would double effective capacity without touching the limit**, which is the fix Alan asked for and I did not ship. The reason it is an issue rather than a commit: the charge sits in a tower middleware and the surviving session is established inside rmcp, so moving it to `on_initialized` would leave the "half-open but un-handshaken" state ungoverned — which is the exact thing `session.rs` documents Mitigation A as existing to prevent. Doing that at 02:00 with the fleet locked out was not a trade worth making. What is unestablished and would decide the design: **why the client posts twice.** Two authenticated POSTs 30 ms apart with different session ids could be protocol-version negotiation, a probe, or an SSE stream open. Nothing in this server's log distinguishes them, because the middleware does not record what the body was. That measurement comes first. Candidate approaches, none evaluated: - Log the JSON-RPC `method` of a fresh POST so the two are distinguishable, then charge only `initialize`. - Collapse charges within a short window per subject, accepting that a burst of N then covers up to N connections rather than N/2. - Leave it, and keep the burst sized at two per expected connection with the ratio written down — which is what `v0.1.5` does.
Author
Owner

Confirmed on v0.1.5, and cleanly

The acceptance window, three connections from three agent sessions:

create new session        6
client initialized        3
orphan creates            0
authenticated requests   23

Exactly 2:1, every create paired, no orphans at all — against 16 creates for
5 sessions with 6 orphans on v0.1.4. Whatever the orphans were on the old pod,
they are not an inevitable part of the two-POST pattern.

02:08:56.748  create      02:08:56.754  create      02:08:56.762  init
02:10:52.128  create      02:10:52.135  create      02:10:52.141  init
02:12:20.893  create      02:12:20.922  create      02:12:20.931  init

And what is not charged, since it was misread once during acceptance

A tool call inside an established session carries mcp-session-id, so
is_fresh_mcp_session_request is false and InitializeLimiter never sees it.
The window makes this directly visible: 23 authenticated requests produced 6
charged creates
, and the other 17 carried a session id and went to the
read/write tool buckets instead.

So a second tool call in the same session does not test this limiter, and a
per-session call count says nothing about initialize capacity. Only a fresh POST
without a session id is charged, and there are two of those per connection.

## Confirmed on `v0.1.5`, and cleanly The acceptance window, three connections from three agent sessions: create new session 6 client initialized 3 orphan creates 0 authenticated requests 23 Exactly 2:1, every create paired, **no orphans at all** — against 16 creates for 5 sessions with 6 orphans on `v0.1.4`. Whatever the orphans were on the old pod, they are not an inevitable part of the two-POST pattern. 02:08:56.748 create 02:08:56.754 create 02:08:56.762 init 02:10:52.128 create 02:10:52.135 create 02:10:52.141 init 02:12:20.893 create 02:12:20.922 create 02:12:20.931 init ## And what is *not* charged, since it was misread once during acceptance A tool call inside an established session carries `mcp-session-id`, so `is_fresh_mcp_session_request` is false and `InitializeLimiter` never sees it. The window makes this directly visible: **23 authenticated requests produced 6 charged creates**, and the other 17 carried a session id and went to the read/write tool buckets instead. So a second tool call in the same session does not test this limiter, and a per-session call count says nothing about initialize capacity. Only a fresh POST without a session id is charged, and there are two of those per connection.
Sign in to join this conversation.
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/carddav-mcp#22
No description provided.