Concurrency burst test: does agent-proxy queue or shed, and should heartbeats carry queue position #107
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#107
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?
Why
#105 established that
queue.waitshows sub-5ms admission delay (0.54ms and 4.03ms across two traces) — but at the observed load an empty queue is the expected result whether queueing is live or inert:Three explanations remain open and span data cannot separate them:
One burst decides it, and decides two downstream questions with it.
Method
Reuse #164's methodology, which ran a 30-way concurrent burst against the Echo-side limiter and observed 8 admitted / 22 rejected with 429.
POST http://ser8:8080/v1/chat/completions(confirmed reachable —GET /v1/modelsreturns 200 in 70ms)Measure
Per trace, from spans:
resilience.attemptstart −queue.waitstart. Baseline is 0.54–4.03ms.Retry-Afteror any queue-depth information is present on rejectionsPer-trace subtraction, not percentile subtraction — #105 documents why comparing p50s across
queue.waitandresilience.attemptis invalid (the latter emits one span per attempt, so retries skew its population).Decision table
queue.waitto match what it measuresqueue.waitrather than rename — it can never show a wait by construction, and backpressure headers become the designCost and timing
A burst issues real model calls and costs real tokens. Keep prompts minimal, and do not run this during an incident — as of 2026-08-12T21:11Z
sirens-echois failing every turn at ~180s whilesirens-deepis healthy, so the current backend state is not representative.Acceptance
queue.wait's rename-or-delete decision both follow from the result rather than from inference.Related
coilyco-gaming/sirens-echo#164— the burst methodology, and evidence that the Echo-side limiter sheds rather than queuesPrecondition: this test is invalid on a contended GPU
Per #108, the local inference host's capacity depends on what else is running on it — a game on 2026-08-12 starved Echo for ~2.5 hours while Deep, on a different tier, was unaffected.
That breaks this test's core measurement. The burst is meant to isolate admission delay under concurrency. On a saturated backend, every request is slow for a reason that has nothing to do with admission, and the decision table in this issue would then read:
— and conclude that queueing exists when what actually happened was that the GPU was busy. A false positive that confirms the hypothesis, which is the worst kind.
Required before running
Also worth noting for whoever runs it: Deep's admission policy rejected roughly one in three of my probe requests today with
> \busy, retry shortly``. A burst will trip that readily. Rejections are a result, not an error — they are precisely what the "does it shed rather than queue" branch of the decision table is looking for — but the harness must record them as such rather than retrying past them.Correction, and measured shed behaviour from traffic that was not mine
My previous comment said "Deep's admission policy rejected roughly one in three of my probe requests" and implied that light probing trips it. Kai pushed on the volume claim; it does not survive checking.
My actual volume was negligible
~9 requests, strictly sequential, spread over ~25 minutes, zero concurrency. That is not pressure, and it should not have shed anything.
The rejections were probably not the admission policy
Spans for
sirens-deepPOST /v1/turnacross my probe window (21:21–21:36 UTC):Three of the four 502s land at exactly 30.00s. My three
busy, retry shortlyreplies align with those, not with the single admission rejection. Sobusy, retry shortlyis most likely a backend failure at a 30s deadline, not shedding — and my attribution of it to the admission policy was wrong.Caveat: these queries hit the 50-row limit, so the counts are a floor rather than a total. The 4:1 ratio is what is visible, not necessarily what occurred.
What the window does show, from someone else's traffic
The endpoint was carrying sustained load I did not generate — a request every 4–11 seconds from 21:22 onward, and a ~30-way concurrent burst at 21:39. That burst is effectively the experiment this issue proposes, run against
sirens-deep's/v1/turnrather than agent-proxy:Paced traffic is never shed. 21:37:03 → 21:39:14, 23 consecutive requests at ~4–14s spacing, 100% admitted.
Concurrent traffic is shed hard and instantly. At 21:39:19: 8 admitted, 16 rejected. At 21:39:59: 10 admitted, 14 rejected. Every rejection is 429 at 0.00s duration — no wait, no queue, immediate.
But admitted requests serialize. Durations across the simultaneously-submitted batch climb monotonically:
That is the signature of a concurrency limit behind the admission gate. So the shape is hard shed at the door, limited concurrency inside — which is neither of the clean branches in this issue's decision table.
Implication for this issue
Two things carry over to the agent-proxy burst:
/v1/turnwas taking background traffic every few seconds throughout. If agent-proxy is similarly loaded during the burst, the results describe the combined load, not the burst. Worth confirming who or what generates that cadence before running.Not run: this one needs your go-ahead and an idle GPU, not a branch
Working through the unblocked issues in this repo, this is the one I deliberately did not do. Three preconditions in the issue body are outside what I can decide:
ser8:8080. A sweep of 1/5/10/30 repeated three times is 138 real model calls, and that is a spend decision.So it is waiting on you rather than on code.
Two things landed that change the run
#109 (
backend-regime-spans) makes the run verifiable after the fact. That was the third acceptance criterion here and it now has a mechanism: setPROXY_BACKEND_REGIME=idlebefore the burst, and every span in the run carriesagentproxy.backend.regime. If it turns out the GPU was not idle, the result can be discarded on evidence instead of argued about. Without that, "was the box quiet" is a memory.#105 (
queue-wait-measures-the-wait) makes the key measurement direct.queue.waitnow closes when a worker claims the job, so admission delay is the span's own duration. The method section here specifiesresilience.attempt start − queue.wait startprecisely because the span used to stay open for the whole request; that subtraction still works, but the span duration is now the simpler and more honest number.One thing landed that will confound the run if you forget it
#110 (
configurable-rate-limits) ships shedding on by default at 1/s with burst 1. A 30-way burst against that limiter answers 429 to 29 of them in milliseconds, and the decision table here would read that as "proxy sheds, never queues" when what it actually measured is the limiter I just added.Set
PROXY_RATE_LIMIT_PER_SECOND=0for the duration of the burst, or the run measures the wrong layer. This is the single most likely way to get a confidently wrong answer out of this test now.Suggested procedure when you want it
PROXY_BACKEND_REGIME=idleandPROXY_RATE_LIMIT_PER_SECOND=0on the proxy.queue.waitduration, and confirm every span in the window carriesregime=idlebefore believing any of it.Happy to write the burst script and drive the analysis on your word. I am not going to spend tokens against live infrastructure without it.