Skip to content

fix(proxy): exclude streaming sessions from latency metrics - #514

Merged
dviejokfs merged 3 commits into
mainfrom
fix/proxy-self-duration-streaming
Aug 2, 2026
Merged

fix(proxy): exclude streaming sessions from latency metrics#514
dviejokfs merged 3 commits into
mainfrom
fix/proxy-self-duration-streaming

Conversation

@dviejokfs

Copy link
Copy Markdown
Contributor

Problem

ProxyMetrics::record derives proxy self time as elapsed - upstream, where elapsed is read in Pingora's logging hook. That hook fires when the session ends, not when the response is served. For an established WebSocket tunnel or an SSE stream that is a connection lifetime, not a latency — a WebSocket held open until the 1h idle timeout set in upstream_peer booked an hour of "proxy overhead" the proxy never spent.

The interval averages are unweighted, so a small number of such sessions ending together can dominate proxy.self_duration_avg_ms. Because those observations sit far out in the tail, the percentiles stay flat while the mean moves — on a chart that is indistinguishable from a genuine latency regression.

Change

  • Streaming sessions (status 101, or an upstream that returned text/event-stream) are still counted as requests, so the status-class and destination counters continue to partition proxy.requests — but they are excluded from every duration histogram.
  • Their lifetime is reported separately via proxy.streaming_sessions and proxy.streaming_duration_avg_ms.
  • Latency averages divide by a new duration_count rather than the request total, so the carve-out cannot dilute them.
  • Console gains a Proxy overhead percentiles panel. self_duration_p50/p95/p99 were already sampled and stored but never charted, which is what made a single-outlier artifact indistinguishable from a real regression.

Evidence

Unit-level — the new regression test fails without the carve-out:

assertion `left == right` failed
  left: 3597.3996003996003     <- one 1h tunnel among 1000 fast requests
 right: 1.0

Runtime — a real temps proxy forwarding to a container serving /, /sse (text/event-stream) and /ws (101 Switching Protocols). 24,027 background requests, plus one WebSocket and one SSE session each held exactly 45s, isolated into separate sampler windows. Raw 30s samples from service_metrics:

sample streaming_sessions streaming_duration_avg_ms self_duration_avg_ms self_p99_ms requests
20:48:07 0 0.0255 4.9507 3141
20:48:37 — WS closed 1 45004 0.0275 4.9505 3132
20:49:07 0 0.0511 4.9529 2761
20:50:07 0 0.1269 4.9584 1805
20:50:37 — SSE closed 1 45001 0.0583 4.9550 2779
20:51:07 0 0.0324 4.9515 2991

Recorded lifetimes are 45004 ms / 45001 ms against a held time of exactly 45s. self_duration_avg_ms stays inside its neighbours' noise band and p99 never leaves the <=5ms bucket.

Without the carve-out those same samples would have read:

case observed would have been inflation
WebSocket 101 0.0275 ms 14.39 ms 524x
SSE 0.0583 ms 16.25 ms 279x

...with p99 unmoved in both cases, since one outlier in ~3000 sits above the 99th percentile.

Console verified in a browser: all four panels render with the expected series counts — Latency breakdown (3), Proxy overhead percentiles (3), Streaming sessions (1), Streaming session lifetime (1).

Not covered

Both runtime tests drove traffic through the console-fallback path, so destination = Console. A deployed project route was not exercised: deployments are queue/webhook-driven with no REST trigger, and hand-inserting route-table rows would have tested the fixture rather than the product. The carve-out is destination-independent by construction — is_streaming comes from the status code and the upstream content-type, and record() treats all three destinations identically — but that is reasoning, not evidence.

Test plan

  • cargo test --lib -p temps-proxy — 433 pass. One unrelated pre-existing failure: test_proxy_route_resolution hardcodes a port base of 9000 and unwrap()s the bind, so it fails with AddrInUse whenever anything occupies that range locally. Passes on a free port; worth fixing separately by binding port 0.
  • cargo clippy -p temps-proxy --all-targets -- -D warnings — clean.
  • tsc --noEmit — clean.

`ProxyMetrics::record` derives proxy self time as `elapsed - upstream`,
where `elapsed` is read in Pingora's `logging` hook. That hook fires when
the session ends, not when the response is served, so for an established
WebSocket tunnel or an SSE stream it measures a connection lifetime rather
than a latency: a WebSocket held open until the 1h idle timeout set in
`upstream_peer` booked an hour of "proxy overhead" the proxy never spent.

The interval averages are unweighted, so a small number of such sessions
ending together can dominate `proxy.self_duration_avg_ms`. Because those
observations sit far out in the tail, the percentiles stay flat while the
mean moves — on a chart that is indistinguishable from a genuine latency
regression.

Streaming sessions are still counted as requests, so the status-class and
destination counters continue to partition `proxy.requests`, but they are
excluded from every duration histogram and reported separately via
`proxy.streaming_sessions` and `proxy.streaming_duration_avg_ms`. Latency
averages now divide by a new `duration_count` instead of the request total
so the carve-out cannot dilute them.

Adds a "Proxy overhead percentiles" console panel: `self_duration_p50/p95/
p99` were already sampled and stored but never charted, which is what made
a single-outlier artifact indistinguishable from a real regression.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

📓 Changelog preview

This is what your commits will add to the generated CHANGELOG.md at release time (via git-cliff). Do not edit CHANGELOG.md by hand — it is generated from your Conventional Commit messages.

## [Unreleased]

### Fixed

- **proxy:** Exclude streaming sessions from latency metrics

@dviejokfs
dviejokfs merged commit 4c40018 into main Aug 2, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant