fix(telemetry): a held-open session's lifetime is not a request latency #581

Merged
coilyco-ops merged 1 commit from fix/a-session-lifetime-is-not-a-latency into main 2026-08-13 16:24:39 +00:00
Member

closes #560

Second half of #160. Kai's original note recorded the consequence:

These alone set the service's HTTP POST p99 to 180s.

Two spans set a service-wide p99.

Why it got worse before it got better

otelhttp names every client span for its method, so the MCP transport and the model path both emitted HTTP POST. A session that exists to stay open sat in the same percentiles as a call that should return in milliseconds.

PR#558 removed the session timeout, which was right — a held-open session has no request to bound. But it means sessions are no longer cut at 180s, so they are free to be long, and every one still landed on the request-path name. #560 predicted that when it was filed, and it is now live on main.

The change

The MCP transport names its spans mcp.session POST. Prefixed rather than replaced, so a reader still sees the verb and one filter on the prefix catches every method at once.

Three tests: the naming, that it cannot collide with the request-path name, and that the operation string otelhttp supplies is ignored — that argument is the name this fix exists to discard, so reading it would reintroduce the bug.

What this does not do

It is not #161 and does not serve it, which I want on the record because I originally said one change plausibly covered both.

#161 asks for the MCP method name on the client span. With streamable HTTP one POST carries many JSON-RPC messages, so there is no single method to attach at the transport layer — the methods are inside a stream body otelhttp never parses. Whatever answers #161 has to live where the calls are dispatched.

I also checked PR#567 before starting, since it touches the same file. It adds mcp.server.name and mcp.discovery.stage to the discovery span, which is a different span. No overlap.

ward exec gate green.

closes #560 Second half of #160. Kai's original note recorded the consequence: > These alone set the service's HTTP POST p99 to 180s. **Two spans set a service-wide p99.** ## Why it got worse before it got better `otelhttp` names every client span for its method, so the MCP transport and the model path both emitted `HTTP POST`. A session that exists to stay open sat in the same percentiles as a call that should return in milliseconds. PR#558 removed the session timeout, which was right — a held-open session has no request to bound. But it means **sessions are no longer cut at 180s, so they are free to be long**, and every one still landed on the request-path name. #560 predicted that when it was filed, and it is now live on main. ## The change The MCP transport names its spans `mcp.session POST`. Prefixed rather than replaced, so a reader still sees the verb and one filter on the prefix catches every method at once. Three tests: the naming, that it cannot collide with the request-path name, and that the `operation` string otelhttp supplies is ignored — that argument is the name this fix exists to discard, so reading it would reintroduce the bug. ## What this does not do **It is not #161** and does not serve it, which I want on the record because I originally said one change plausibly covered both. #161 asks for the MCP method name on the client span. With streamable HTTP **one POST carries many JSON-RPC messages**, so there is no single method to attach at the transport layer — the methods are inside a stream body `otelhttp` never parses. Whatever answers #161 has to live where the calls are dispatched. I also checked PR#567 before starting, since it touches the same file. It adds `mcp.server.name` and `mcp.discovery.stage` to the *discovery* span, which is a different span. No overlap. `ward exec gate` green.
fix(telemetry): a held-open session's lifetime is not a request latency
All checks were successful
ci / image-build (pull_request) Successful in 27s
ci / test (pull_request) Successful in 42s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
6ff5181eca
otelhttp names every client span for its method, so the MCP transport and the
model path both emitted HTTP POST. A session that exists to stay open was
therefore counted in the same percentiles as a call that should return in
milliseconds, and two of them set the service's HTTP POST p99 to 180 seconds.

Removing the session timeout fixed sessions dying at 180s and made this worse
rather than better, exactly as sirens-echo#560 predicted: sessions are now free
to be long, and every one of them still landed on the request-path name.

The MCP transport names its spans mcp.session plus the verb. Prefixed rather
than replaced, so a reader still sees the method and one filter catches every
verb at once.

This is not the method-name request in sirens-echo#161 and does not serve it.
One streamable POST carries many JSON-RPC messages, so there is no single
method to attach at the transport layer. That one has to live where the calls
are dispatched.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
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
coilyco-gaming/sirens-echo!581
No description provided.