Found while implementing the transport-owned http_requests_total seam for #9650 (PR #9746). Filed unassigned; not graded by the filing seat.
The claim, measured on 8957c72227
With the transport seam of PR #9746 installed and one registry handed to both the transport and the dispatcher — which is what a host that wires observability once does — the dispatcher's own routes are counted twice, and only the dispatcher's.
Measured, one request each, one shared InMemoryMetricsRegistry:
| route | requests | counted |
|---|
/.well-known/objectstack (dispatcher's own) | 1 | 2 |
/api/v1/auth/* (raw-app mount) | 1 | 1 |
Pinned as an executable measurement in packages/runtime/src/http-metrics-inbound-coverage.hono.integration.test.ts, section 4, "the dispatcher route is counted TWICE while its own Proxy still emits".
That asymmetry is the harm: a uniform scale factor could be divided out, a per-surface one silently distorts the ratio between surfaces, which is exactly what the 5xx-rate and traffic-share guidance in the docs reads.
Mechanism
Two emitters, identical metric name, identical labels:
For the dispatcher's routes the two label sets are the same string, so the samples merge into one series at twice the true rate.
Why it was not folded into #9650
The maintainer's 2026-08-18 ruling puts the counter on the transport, so the dispatcher's copy is the duplicate. But the emission is not separable from the rest of instrumentRouteHandler, which also:
- resolves and echoes the request id (
X-Request-Id); - emits
http_request_duration_ms; - emits
http_request_errors_total; - calls the
ErrorReporter on 5xx, including the res.__obsRecordedError side channel.
packages/runtime was outside #9650's dispatched file surface, and dropping the whole wrapper would drop all four of those behaviours. So this is reported rather than widened, per the dispatch instruction.
Repair options (not chosen here)
- Give
instrumentRouteHandler an opt-out for the counter alone and have the dispatcher pass it. Needs someone to decide when — the dispatcher cannot detect whether the transport under it implements the seam. - Land the
IHttpServer response-observing contract hook first (the ruled successor, filed separately), then the dispatcher can ask the transport whether it already counts. - Leave
instrumentRouteHandler as the request-id/duration/error path and strip only its httpRequestsTotal line, accepting that a host composing the runtime without a counting transport loses the counter entirely.
Option 2 subsumes this; option 1 is the cheap interim. Both are decisions above the seat that measured this.
Blast radius today
Zero in a shipped objectstack serve deployment, because the transport there resolves no registry at all — see the CLI-wiring card. The duplicate appears the moment that wiring lands, so the two should be sequenced together.
Generated by Claude Code
Found while implementing the transport-owned
http_requests_totalseam for #9650 (PR #9746). Filed unassigned; not graded by the filing seat.The claim, measured on
8957c72227With the transport seam of PR #9746 installed and one registry handed to both the transport and the dispatcher — which is what a host that wires observability once does — the dispatcher's own routes are counted twice, and only the dispatcher's.
Measured, one request each, one shared
InMemoryMetricsRegistry:/.well-known/objectstack(dispatcher's own)/api/v1/auth/*(raw-app mount)Pinned as an executable measurement in
packages/runtime/src/http-metrics-inbound-coverage.hono.integration.test.ts, section 4,"the dispatcher route is counted TWICE while its own Proxy still emits".That asymmetry is the harm: a uniform scale factor could be divided out, a per-surface one silently distorts the ratio between surfaces, which is exactly what the 5xx-rate and traffic-share guidance in the docs reads.
Mechanism
Two emitters, identical metric name, identical labels:
packages/runtime/src/observability/instrument.ts:106via theProxyatpackages/runtime/src/dispatcher-plugin.ts:700-721, labelled with the registered route string;installHttpMetricsSeam(PR fix(hono-server): emit http_requests_total from the transport so every inbound mount is counted (#9650) #9746), labelled with the matched pattern.For the dispatcher's routes the two label sets are the same string, so the samples merge into one series at twice the true rate.
Why it was not folded into #9650
The maintainer's 2026-08-18 ruling puts the counter on the transport, so the dispatcher's copy is the duplicate. But the emission is not separable from the rest of
instrumentRouteHandler, which also:X-Request-Id);http_request_duration_ms;http_request_errors_total;ErrorReporteron 5xx, including theres.__obsRecordedErrorside channel.packages/runtimewas outside #9650's dispatched file surface, and dropping the whole wrapper would drop all four of those behaviours. So this is reported rather than widened, per the dispatch instruction.Repair options (not chosen here)
instrumentRouteHandleran opt-out for the counter alone and have the dispatcher pass it. Needs someone to decide when — the dispatcher cannot detect whether the transport under it implements the seam.IHttpServerresponse-observing contract hook first (the ruled successor, filed separately), then the dispatcher can ask the transport whether it already counts.instrumentRouteHandleras the request-id/duration/error path and strip only itshttpRequestsTotalline, accepting that a host composing the runtime without a counting transport loses the counter entirely.Option 2 subsumes this; option 1 is the cheap interim. Both are decisions above the seat that measured this.
Blast radius today
Zero in a shipped
objectstack servedeployment, because the transport there resolves no registry at all — see the CLI-wiring card. The duplicate appears the moment that wiring lands, so the two should be sequenced together.Generated by Claude Code