Skip to content

The dispatcher's instrumentRouteHandler Proxy double-counts http_requests_total for its own routes once the transport seam emits #9833

Description

@huangyiirene

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:

routerequestscounted
/.well-known/objectstack (dispatcher's own)12
/api/v1/auth/* (raw-app mount)11

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)

  1. 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.
  2. Land the IHttpServer response-observing contract hook first (the ruled successor, filed separately), then the dispatcher can ask the transport whether it already counts.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions