feature: latency whiskers hide the slow requests — no max, p95 or p99, and the chart's top is the highest whisker
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 13
- Forks
- 40
- Avg merge
- 12h 17m
- Merged PRs (30d)
- 156
Description
Problem
The latency view in abctl's Usage pane (u, then m to latency) draws mean-with-whiskers — ┼ mean, ┬/┴ ±1σ. That answers "what is typical, and how much does it vary", but it gives the operator no way to see the slow requests, which is usually why they opened a latency chart in the first place.
Concretely: there is currently an event with duration 120.25s (11,185 tokens, against api.anthropic.com), and the latency chart's box and whiskers only reach about 22s. The slowest request in the window is not merely off to one side of the chart — it is above the top of the frame, and nothing on screen says so.
Why it happens
The chart's ceiling is the highest +1σ cap, not the highest observed duration — authbridge/cmd/abctl/tui/usage_whiskers.go:
var peak float64
for _, r := range rows {
if r.hi > peak { // r.hi == mean + stddev
peak = r.hi
}
}
Everything is scaled from that peak: the axis gridline labels, whiskerCell's row positions, and the clamp if n > plotRows { n = plotRows }. So the top of the plot is the tallest whisker, by construction.
The deeper cause is that no maximum is recorded anywhere in the pipeline. The accumulator keeps only a sum, a sum of squares and a count (authbridge/authlib/usage/usage.go):
latSum float64 // milliseconds
latSumSq float64 // milliseconds squared, for stddev
latN int64
and the wire type exposes exactly three latency fields — LatMeanMs, LatStdDevMs, LatSamples. There is no max, no percentile, and no histogram, in the aggregator, on the wire, or in the renderer. The value row printed under each whisker (renderLatencyValues) shows the mean, so the 120.25s request has no numeric representation in this view either.
Why +1σ is not a stand-in for the max
Latency is right-skewed, so a single slow request is diluted into both the mean and σ — and the more traffic shares its bucket, the further the chart's top drifts below the real maximum. Taking the reported 120.25s outlier plus some number of ordinary ~2s requests in the same bucket:
| requests in bucket | mean | σ | chart ceiling (mean+σ) | actual max |
|---|---|---|---|---|
| 2 | 61.12s | 59.12s | 120.25s | 120.25s |
| 10 | 13.82s | 35.48s | 49.30s | 120.25s |
| 20 | 7.91s | 25.77s | 33.68s | 120.25s |
| 40 | 4.96s | 18.46s | 23.42s | 120.25s |
At ~40 requests the ceiling lands at 23.4s, which matches the ~22s being observed. Under a normal distribution +1σ covers only ~84% of samples; for a heavy tail it is far less, so the whisker cap should not be read as "the worst case" — and today there is nothing else to read instead.
This makes the chart actively misleading rather than merely incomplete: an operator scanning it concludes the workload's worst latency is ~22s when one request took 5.5x that.
Proposed solution
Record and surface the tail. Roughly:
- Aggregator —
authbridge/authlib/usage/usage.gohas one place where a duration is observed (b.latSum += ms; b.latSumSq += ms*ms; b.latN++), which is where a running max belongs. A true max is oneif ms > b.latMaxand costs nothing. - Wire — add to
usage.Bucketasomitempty, so an older proxy simply omits it and the client falls back to today's behaviour. - Fold —
snapshot.gomerges buckets; the max folds as a max-of-maxes, not weighted the way the mean is. - Renderer — scale
peakto include the max, and give it its own glyph so it is distinguishable from the ±1σ caps. The per-bucket value row could show mean and max rather than mean alone. - README — the Panes section currently documents only
┼/┬/┴.
Percentiles (p95/p99) are the better statistic — a true max is a single sample and jumps around between refreshes — but they need either reservoir sampling or histogram buckets in the aggregator, which is a substantially larger change than a running max. A reasonable path is max first (cheap, exact, and immediately fixes the misleading ceiling), then p95/p99 if the histogram is judged worth its memory. Worth deciding deliberately, since the wire format is involved either way.
Additional context
- The 22s-vs-120.25s figures above are from a live session; the table is the arithmetic that reproduces them, not measured data.
- Related:
b(breakdown) is already unavailable for latency because the aggregator holds no per-label latency — the same "latency stats are thinner than the count stats" theme. - Not a regression; the chart has behaved this way since the whisker renderer was introduced.
Assisted-By: Claude (Anthropic AI) noreply@anthropic.com
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read authbridge/authlib/usage/usage.go and the usage.Bucket and snapshot.go paths to trace latency aggregation and folding, then inspect authbridge/cmd/abctl/tui/usage_whiskers.go and the README Panes section. Done means the maximum is preserved through the wire and snapshot, included in chart scaling with a distinct glyph, represented in the value row, and documented, while older proxies retain the existing fallback.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli, observability
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100