feat(proxy): per-worker admission control that rejects excess requests with 503 - #39352
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
|
PR #39352 (BerriAI/litellm, author devin-ai-integration[bot]) has no |
Greptile SummaryThe PR adds opt-in, per-worker admission control that bounds active and queued HTTP requests while exempting operational probe and metrics routes.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/proxy/middleware/admission_control_middleware.py | Implements the admission gate, root-path-aware exemptions, injected process-local state, cached validated settings, metrics, and overload responses; the previously reported issues are addressed. |
| litellm/proxy/proxy_server.py | Registers the admission middleware around the proxy and exposes the new settings through general-settings metadata. |
| litellm/proxy/health_endpoints/_health_endpoints.py | Extends the authenticated backlog endpoint with admitted, queued, and rejected request statistics. |
| litellm/proxy/_types.py | Adds typed and constrained configuration fields for the per-worker admission limits and queue timeout. |
| tests/test_litellm/proxy/middleware/test_admission_control_middleware.py | Covers capacity, queue ordering, timeout and cancellation cleanup, prefixed exemptions, streaming lifetime, metrics, invalid settings, and state isolation. |
| tests/load_tests/test_granian_admission_saturation.py | Adds an opt-in single-worker saturation benchmark validating overload responses and liveness latency. |
Reviews (2): Last reviewed commit: "refactor(proxy): simplify invalid admiss..." | Re-trigger Greptile
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ttings, inject state Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Fixed in 8d3e9c7: the middleware now strips |
|
Fixed in 8d3e9c7: negative queue size and nonpositive timeout are rejected via constrained Pydantic adapters and |
|
Fixed in 8d3e9c7: the overload response is now a Starlette |
|
Fixed in 8d3e9c7: counters and semaphore moved into an injected |
…fix lookalike paths Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…rsing, log invalid limits Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai please re-review: new arrivals now queue behind pending waiters, settings parsing is cached, invalid limits are logged |
Description
TLDR
Problem this solves:
global_max_parallel_requestsneeds Redis and is ignored by the v3 limiter (LIT-5460)How it solves it:
overloaded_errorwithretry-after: 1SERVER_ROOT_PATH/health/backlogfieldstests/load_tests/Configuration (all under
general_settings, disabled unlessmax_in_flight_requests_per_workeris set):The limit is per worker process, so a pod with
--num_workers 4admits 4x that. It works the same on uvicorn and Granian and complements the Redis-backedglobal_max_parallel_requests: that one bounds a whole deployment, this one keeps any single event loop from drowning while an HPA catches up. Docs PR to follow in litellm-docsUser Flow
Before: a spike lands on a one-worker Granian pod and every caller just waits
--run_granian --num_workers 1and a load spike sends 200 concurrent POST https://litellm-domain/v1/chat/completions{"in_flight_requests": 200}After: the same spike gets a bounded number of requests through and tells the rest to retry
max_in_flight_requests_per_worker: 8(and optionally the queue size and timeout) togeneral_settings, restarts, and the same 200 concurrent POST https://litellm-domain/v1/chat/completions arrive503 {"error":{"message":"Worker at capacity: 8 in-flight, 8 queued requests. Retry later.","type":"overloaded_error","code":"503"}}with aretry-after: 1headerin_flight_requests,admitted_requests,queued_requests, andrejected_requests, and/metricsexposeslitellm_admission_admitted_requests,litellm_admission_queued_requests, andlitellm_admission_rejected_requests_total{reason="queue_full"|"queue_timeout"}Relevant issues
Linear ticket
Resolves LIT-6561
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live proxy, one Granian worker, real Anthropic calls. Config used for both runs (key values omitted):
Server:
python litellm/proxy/proxy_cli.py --config proof_config.yaml --run_granian --num_workers 1 --port 4000Before (55e9e4c)
Six concurrent completions
for i in $(seq 6); do curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" http://localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-..." -H "Content-Type: application/json" -d '{"model":"claude","messages":[{"role":"user","content":"Write a 300 word story"}]}' & done; waitBacklog and metrics
curl -s http://localhost:4000/health/backlog -H "Authorization: Bearer sk-..."{"in_flight_requests":1}curl -sL http://localhost:4000/metrics -H "Authorization: Bearer sk-..." | grep litellm_admissionreturns nothingAfter (6131186, behavior identical to a5c5c20 where this run was captured; later commits add
/metrics/to the exempt paths, striproot_pathbefore the exempt check, log and ignore invalid settings, move counters into an injectedAdmissionControlState, route new arrivals behind pending queue waiters instead of past them, and cache settings parsing off the hot path, plus extra tests)Six concurrent completions
curl -siwhile 3 others are in flight):Streaming holds a slot
"stream": truerequests piped throughhead -c 200, then a third non-stream request 100ms laterBacklog and metrics
curl -s http://localhost:4000/health/backlog -H "Authorization: Bearer sk-..."{"in_flight_requests":1,"admitted_requests":0,"queued_requests":0,"rejected_requests":7}(the one in-flight request is the backlog call itself, which is exempt)curl -sL http://localhost:4000/metrics -H "Authorization: Bearer sk-..." | grep litellm_admissionSaturation benchmark
LITELLM_RUN_SATURATION_BENCHMARK=1 uv run --no-sync pytest tests/load_tests/test_granian_admission_saturation.py -s(one Granian worker, 200 concurrent requests against a local fake OpenAI endpoint, cap 8 in-flight and 8 queued, 0.5s queue timeout)Type
🆕 New Feature
Caveats (if any)
Medium
max_in_flight_requests_per_workerLow
/health(model health check) is not exempt, only the probe paths areFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/079cbff62c9e41e38376d31c27df4dcf
Open in Devin Desktop: https://app.devin.ai/desktop/session/079cbff62c9e41e38376d31c27df4dcf?variant=devin
Requested by: @yassin-berriai