CVE-2026-107286: Pydantic AI: Concurrency-limited models can keep their slot when a streamed request ends early
This issue was posted by Codex Desktop using gpt-6.1-sol on behalf of David.
Summary
Applications that wrap a model with ConcurrencyLimitedModel or limitmodelconcurrency can permanently lose shared concurrency capacity when a streamed request releases its slot from a different task than the one that acquired it. This can happen when a stream ends early, and also when a stream is fully consumed using the default streamtext() debouncing.
In an application that exposes an affected streaming endpoint to network clients and shares a long-lived model limiter across requests, a client can repeatedly start a stream and disconnect. The completed requests retain their slots, eventually preventing subsequent requests that share the limiter from proceeding.
Agent-level maxconcurrency and non-streaming model requests are not affected by this defect.
Details
The built-in limiter uses anyio.CapacityLimiter, which associates each acquired slot with its borrowing task. Pydantic AI's streaming lifecycle can acquire the slot on the task consuming the stream and run cleanup on another internal task. The limiter rejects that release, so the slot remains occupied even after the request has ended. Cleanup can raise a RuntimeError; a later request on the borrowing task can also fail because that task still holds a slot.
Early termination includes stopping iteration, a consumer exception, and cancellation. Fully consuming streamtext() with its default debounceby=0.1 can also reach the cross-task release path. Fully consumed streams must therefore not be assumed safe.
Mitigation
Upgrade to a patched release of pydantic-ai or pydantic-ai-slim. If you cannot upgrade yet, use the agent-level maxconcurrency setting instead of a concurrency-limited model, or avoid streaming runs through a concurrency-limited model.
Other sources
Pydantic AI is a Python agent framework for building applications and workflows with Generative AI. From 2.10.0 until 2.53.0, streamed requests made through ConcurrencyLimitedModel or limitmodelconcurrency can retain shared concurrency slots because anyio.CapacityLimiter associates an acquired slot with the borrowing task while streaming cleanup can run in a different task. Early stream termination, cancellation, consumer exceptions, or complete streamtext() consumption with debounceby=0.1 can therefore leave capacity occupied, eventually preventing later requests that share the long-lived limiter from proceeding and causing a denial of service. Agent-level maxconcurrency and non-streaming model requests are not affected. This issue is fixed in version 2.53.0.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/pydantic-ai-slimto a version that resolves this vulnerability.Fixed in 2.53.0 - Upgrade
Upgrade
pip/pydantic-aito a version that resolves this vulnerability.Fixed in 2.53.0 - Upgrade
Upgrade
pydantic-ai or pydantic-ai-slimto a version that resolves this vulnerability.Fixed in 2.53.0 - Compensating control
If upgrading is not yet possible, use the agent-level max_concurrency setting instead of a concurrency-limited model, or avoid streaming runs through a concurrency-limited model.
Event History
Frequently Asked Questions
Which deployments are exposed to this denial-of-service condition?
Deployments using Pydantic AI versions 2.10.0 through versions before 2.53.0 are affected when streamed requests use ConcurrencyLimitedModel or limit_model_concurrency with a shared, long-lived limiter. Agent-level max_concurrency and non-streaming model requests are not affected.
What request behavior can trigger exhaustion of concurrency slots?
A streamed request can retain its slot when it ends early due to cancellation, consumer exceptions, or early stream termination. Complete stream_text() consumption can also retain a slot when debounce_by is set to 0.1.
What is the practical impact once slots are retained?
Retained slots consume the shared limiter's available capacity. Over time, later requests sharing that long-lived limiter may be unable to proceed, causing denial of service.
What remediation is available?
Upgrade Pydantic AI to version 2.53.0, which fixes the issue.