Median turn spends 20s to do 3.4s of work — establish what queue.wait actually measures, then fix it #105
Labels
No labels
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/agent-proxy#105
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Observation
24h window, scalar aggregates:
POST /v1/chat/completionsrequest.chatqueue.waitresilience.attemptupstream.chatlitellm_request(litellm)At p50 there is a 16.6s gap between
queue.waitand theresilience.attemptnested inside it. At p99 the same two spans are only 1.5s apart.What I am not claiming
If
queue.waitis the parent ofresilience.attempt, its duration spans admission through completion rather than time spent waiting, and the name is then misleading rather than the behaviour being wrong. Percentiles across different requests do not decompose, so p50-minus-p50 is not a per-request wait and I cannot settle this from aggregates.The p50 and p99 rows disagree about which reading is right, which is itself the reason to look: a consistent story would show the same relationship at both.
Why it matters either way
coilyco-gaming/sirens-echo#160).queue.waitis unusable for exactly the question people will ask it, and the queue-position signal proposed in #104 has less to report than that issue assumes.Both outcomes change what gets built next, which is why this is worth an hour before the heartbeat work rather than after.
Suggested first step
Read 5–10 individual traces rather than aggregates: confirm the parent/child relationship, and measure enqueue→first-attempt-start per trace. That is a direct measurement of admission delay and settles it.
Acceptance
queue.waitare documented, and its name matches what it measures.Related
coilyco-gaming/sirens-echo#162— where these numbers were first gatheredResolved by direct measurement: there is no admission delay, and
queue.waitis misnamedRead two traces end to end. Both give the same structure, and it is not what the aggregates implied.
queue.waitis a sibling ofresilience.attempt, not its parentI guessed parent/child in the issue body. It is neither — they are siblings that both span the whole request.
Admission delay, measured per trace
Time from
queue.waitstart toresilience.attemptstart:queue.waitstartresilience.attemptstart724205380aa14e8fa7806c0ab789f19a(fast, 1.16s)20:40:05.71952556620:40:05.7235508578ea6619aa21aacae118b2d310a658f11(slow, 240.0s)20:03:18.21851897120:03:18.219062706Sub-5ms in both, including the 240-second request. There is no queueing to report.
And the durations confirm the naming problem:
queue.wait1.160102s againstrequest.chat1.161064s in the fast trace, 239.9856s against 239.9864s in the slow one. It tracks the whole request to within a millisecond.Where the 16.6s p50 gap came from
A population artifact, as feared.
resilience.attemptemits one span per attempt, so retries put multiple short spans into its population — the 19response_validation_failedattempts in this window are ~3s each.queue.waitemits once per request. Comparing p50s across span names with different per-request cardinality is invalid, and that is exactly what I did.Retracting the claim that the median turn spends ~17s queueing to do ~3.4s of work. It does not. Correction posted to
coilyco-gaming/sirens-echo#162, where I made it most strongly.What this changes downstream
#104's heartbeat payload should drop queue position. There is no queue depth worth reporting. The
attempt n of 3signal survives and is now the main justification — retries are real, they are invisible from outside, and 19 of them happened in 24h.Recommended fix here
Rename
queue.wait, or delete it. It measures the same interval asrequest.chatwhile carrying a name that asserts something false about where time goes — which cost this investigation a wrong turn and a retracted claim in two other issues. If admission delay is worth measuring, the span should cover only enqueue→dequeue, which by this data would be a sub-millisecond span most of the time.A separate and more serious finding from the slow trace
Filing separately: in
8ea6619aa21aacae118b2d310a658f11, litellm ran for 447.25s and returned 500, while agent-proxy's client span ended at 239.99s markedhas_error: false. Upstream kept working 207s after the caller stopped listening, and the failure is invisible on the agent-proxy side.Narrowing my own conclusion: I measured no contention, not no queue
I wrote "there is no queueing to report." That is stronger than the evidence supports, and since it drove a recommendation to drop queue position from #104, correcting it now.
The traffic makes an empty queue the expected result either way
Agent-proxy's own 24h figures: 407 requests,
avgDuration23.7s.At ~0.11 average concurrency there is almost never more than one request in flight. A fully functional queue would show sub-millisecond admission delay under that load — indistinguishable from an inert one. My two traces are consistent with:
Span data cannot separate these. I should have said so rather than picking the strongest reading.
What still holds
Unchanged, because it does not depend on load:
queue.waitis a sibling ofresilience.attempt, not its parentrequest.chatto within a millisecond, so it does not measure a wait whatever the queue is doingThe naming problem is real regardless of which of the three explanations is correct.
What changes
Withdrawing "drop queue position from #104." The correct statement is narrower: queue position carries no information at current traffic levels. Whether it carries information under concurrency is untested — and the concurrency case is the one that matters, because several people hitting the bot at once is precisely the August 19 scenario.
The test that settles it
A concurrency burst against agent-proxy, reusing #164's methodology (that issue ran a 30-way burst against the Echo-side limiter and got 8 admitted / 22 rejected with 429). Two questions, both answered by one run:
queue.waitcan never show a wait by construction, and the span should be deleted rather than renamed.Worth doing before the heartbeat work, not after — same argument as the original issue, now with a sharper test.
Ran the per-trace measurement this issue asks for. It settles the question:
queue.waitis a span-lifetime artifact, not real queueing.Parent/child, settled
queue.waitis not the parent ofresilience.attempt. They are siblings, both children ofrequest.chat:Trace
5d8cdf1209b63818179bd7af3b608b8d.queue.waitstarts 1.55 ms beforeresilience.attemptand ends 46 us after it. It stays open for the whole request instead of closing at dequeue.Per-trace admission delay, 29 traces
Because the two siblings end together,
queue.wait - resilience.attemptis the per-trace admission delay, with no percentile subtraction:queue.waitrangeThe delay stays inside a 0.7-4.5 ms band while
queue.waititself varies 25-fold:That constancy is the proof. Were
queue.waitgenuine waiting andresilience.attemptthe work, the two would be sequential and their difference would scale with load. A near-constant few-ms difference across a 25x duration spread is the signature of two spans that start milliseconds apart and end together.So
queue.waitreports admission delay plus the entire attempt. Reading it as wait time overstates queueing by three to four orders of magnitude.What this does to the p50 gap
The 16.6s p50 gap in the issue body is not 16.6s of queueing. It is the same end-alignment seen through percentiles that do not decompose. The method above computes it per trace, which is what the acceptance criteria asked for.
Worth noting this run was not contention-free, so the low numbers are not an idle-system artifact: the window holds 29 agent-proxy traces against only 12 model calls from the battery, so other traffic was in flight, including one 46.6s request. Admission delay still never exceeded 4.52 ms.
Suggested fix
End the
queue.waitspan at dequeue rather than at request completion. It then measures what its name claims, andrequest.chat - queue.wait - resilience.attemptdecomposes cleanly. Until that lands, no dashboard or alert can usequeue.waitfor saturation: it tracks total request duration within ~1 ms at every percentile, so a saturated proxy and a slow model look identical.This also revises what the heartbeat work can report. Queue position at #104 has close to nothing to carry while real admission delay sits at 1.5 ms. The user-visible 20-80s Discord latencies referenced from coilyco-gaming/sirens-echo#160 are upstream inference time, not admission, so they need a different line of attack.
Provenance
Sirens Deep battery,
ward exec eval-deep, routesirens-echo/deepseek, 10/10 cases pass, exit 0, 12 model calls all HTTP 200. Window1786617849573-1786617990000. Client waskais-macbook-proover the tailnet againstser8:8080. Measured through the SigNoz MCP against traces, not logs.