Skip to content

feat(proxy): reject at capacity instead of queueing unboundedly#143

Merged
nilsmechtel merged 4 commits into
mainfrom
fix/proxy-reject-when-busy
Jul 16, 2026
Merged

feat(proxy): reject at capacity instead of queueing unboundedly#143
nilsmechtel merged 4 commits into
mainfrom
fix/proxy-reject-when-busy

Conversation

@nilsmechtel

Copy link
Copy Markdown
Collaborator

What

When an app's ProxyDeployment semaphore is full, the per-method proxy function now raises a RuntimeError telling the caller the app is at capacity, instead of silently awaiting a free slot.

Application '<id>' has reached its maximum of N concurrent requests and is currently very busy. Please try again in a moment.

The cap is the existing per-app max_ongoing_requests knob (deploy-time deploy_app(max_ongoing_requests=…), default 10; decorator default @bioengine.app(max_ongoing_requests=…)). No new parameter.

Why

Every user call (WebSocket + WebRTC) funnels through deployment_function, which held async with self.service_semaphore. When full, request N+1 awaited a slot — an unbounded queue of coroutines, each already holding its decoded payload (e.g. an inference image) in the proxy actor's memory. Under load this grows proxy memory without bound and gives the caller no signal. Failing fast returns capacity pressure to the caller and frees the payload immediately.

Behaviour changes from "queue when full" to "reject when full" for every app. It does not touch the inner Entry/Runtime deployments' own Ray Serve max_ongoing_requests (the autoscaling-signal queue) — only the proxy's Hypha-facing semaphore.

Safety

.locked() is race-free here: because we reject rather than await, the semaphore never accumulates waiters, so .locked() reduces to "no free slot", and there is no await between the check and the acquire.

Validation

Deployment-side change → validated on a dev image on a live cluster before marking ready (deploy an app with max_ongoing_requests=1, fire concurrent calls, confirm the excess returns the busy RuntimeError while in-flight calls still succeed).

🤖 Generated with Claude Code

nilsmechtel and others added 3 commits July 16, 2026 10:40
When an app's ProxyDeployment semaphore is full, the deployment function
now raises RuntimeError("...has reached its maximum of N concurrent
requests and is currently very busy. Please try again in a moment.")
instead of awaiting a free slot.

A blocked caller holds its already-decoded payload (e.g. an inference
image) in the proxy actor's memory while it waits, so an open-ended
queue grows proxy memory without bound. Failing fast returns capacity
pressure to the caller and frees the payload immediately. The cap is the
existing per-app max_ongoing_requests knob; behaviour changes from
"queue when full" to "reject when full" for every app.

.locked() is race-free here: because we reject rather than await, the
semaphore never accumulates waiters, and there is no await between the
check and the acquire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "maximum of N concurrent requests" wording already conveys that the
app is at capacity; the extra "currently very busy" was redundant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nilsmechtel
nilsmechtel marked this pull request as ready for review July 16, 2026 09:03
@nilsmechtel
nilsmechtel merged commit 7283e83 into main Jul 16, 2026
1 check passed
@nilsmechtel
nilsmechtel deleted the fix/proxy-reject-when-busy branch July 16, 2026 09:05
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