🌏 中文版
Release Info
| Item | Value |
|---|---|
| Framework | Pydantic AI |
| Version | v2.53.0 |
| Previous | v2.52.0 |
| Released | 2026-10-01 |
| Release Notes | GitHub Release |
| Security Advisory | GHSA-6fqq-452j-qhrp |
| GitHub | pydantic/pydantic-ai |
| Stars | 20.4k |
Why this release matters
This security issue is sneakier than the one patched last release (2.52.0's web_fetch nested-HTML DoS), because it doesn't need any malicious input to trigger. ConcurrencyLimitedModel and limit_model_concurrency let multiple agent calls share one concurrency cap — normally, a request finishes and hands its slot back. But if the consumer exits a stream early (stops iterating, raises, or gets cancelled), or fully drains stream_text() with its default debouncing, the slot's release can end up happening on a different task than the one that acquired it. In the current implementation, that cross-task release can leave the slot stuck in a state where it thinks it was released but wasn't. Repeat that pattern a few times and every request sharing the same limiter gets blocked — your own concurrency control turns into the bottleneck. The advisory rates it high; agent-level max_concurrency and non-streaming requests are unaffected, but any team seriously using ConcurrencyLimitedModel for production traffic control should upgrade now. Another signal worth noting: ToolCallJudge lands in this release — a review layer inserted before a tool call actually executes. That's the same underlying question as the concurrency fix, just applied to a different resource: should this action be allowed to happen, and who gets to decide.
What changed
ConcurrencyLimitersemantics changed:acquire()now takes a new slot on every call, even within the same task, and a customAbstractConcurrencyLimitermust now allowrelease()from another task → fixes the root cause of slots getting stuck on cross-task release- Shared limiters now raise, not silently misbehave: a model wrapper raises
UserErrorwhen it shares a limiter with the agent making the request, or with an enclosing wrapper → turns a latent deadlock risk into an explicit error, forcing slot ownership to be designed up front ToolCallJudge(#9041): inserts a review layer right before a tool call executes, so a call can be blocked before it runs → the next concrete step, after guardrails, toward "intercept before execution" as an agent safety mechanismrepair_messagesgoes public (#8370): the message-history repair pipeline, previously internal, is now a public API → teams assembling their own message history by hand can reuse the framework's repair logic instead of rebuilding itAbsurdDurability(#8946): added to the harness, replacing the previously independentpydantic-ai-absurd→ one more durable-execution backend folded back into the main repoSystemOneModel(#8942): lets decision models like CLM and Laya run through the/v1/systemoneAPI → fills in a call shape for fast decision/classification models, distinct from ordinary chat modelspydantic-clai2's plugin ecosystem keeps growing: this release adds built-in posthog, grain, linear, and herdr plugins, plus a Logfire telemetry settings menu,/updatewith stable/bleeding channels, and managed subagents (built-in Claude and Codex agent definitions) → continuing last release's push toward turning the CLI into a real product, with plugin count and coverage still expanding
Breaking Changes
ConcurrencyLimiter.acquire()behavior change:- Before: multiple
acquire()calls within the same task could share or stack the same slot's accounting - After: every call takes a new slot; a custom
AbstractConcurrencyLimitermust supportrelease()from another task - Affected: projects with a custom
AbstractConcurrencyLimitersubclass, or any code relying on the old slot-accounting details
- Before: multiple
- Sharing a limiter between a model wrapper and the agent making the request, or an enclosing wrapper, goes from a silent risk to an explicit
UserError:- Affected: any setup that passed the same
ConcurrencyLimiterinstance into both the agent and a model wrapper will now error at startup or runtime, and needs to split into separate limiter instances
- Affected: any setup that passed the same
Migration Guide
Upgrading from 2.52.0 to 2.53.0
pip install --upgrade pydantic-ai==2.53.0
# Before (2.52.0 and earlier, agent and wrapper share one limiter instance)
limiter = ConcurrencyLimiter(max_concurrency=5)
model = ConcurrencyLimitedModel(base_model, limiter=limiter)
agent = Agent(model, model_settings=ModelSettings(concurrency_limiter=limiter)) # shared limiter
# After (2.53.0, agent and wrapper each hold their own limiter)
agent_limiter = ConcurrencyLimiter(max_concurrency=5)
wrapper_limiter = ConcurrencyLimiter(max_concurrency=5)
model = ConcurrencyLimitedModel(base_model, limiter=wrapper_limiter)
agent = Agent(model, model_settings=ModelSettings(concurrency_limiter=agent_limiter))
Projects without a custom AbstractConcurrencyLimiter subclass, and without the same limiter instance passed into both an agent and a model wrapper, have no code-level breaking change. The ConcurrencyLimitedModel security fix itself requires no code change — upgrading is enough to patch it.
How this compares to other frameworks
This is the second release in a row where Pydantic AI patches a resource-exhaustion security issue — last time it was HTML parsing that could be pushed to exhaust CPU/memory, this time it's a concurrency slot that can get stuck and jam an entire limiter. The same pattern shows up in crewAI's recent releases: in September it bumped both pypdf and nltk for CVEs in the same release, and in August it patched a batch of streaming-response edge cases. That points to a shared pressure point — agent frameworks that support long-running execution, concurrent calls, and external content fetching inevitably accumulate more shared internal state (connection pools, limiters, parser state), and any exception path that isn't handled cleanly (early cancellation, cross-task release, adversarial input) becomes a triggerable DoS surface. For teams evaluating agent frameworks, how often this class of bug shows up and how fast it gets patched is itself a signal worth weighing — not just feature count.
Today's takeaway
I used to think of a concurrency limiter as a simple counter problem: take a slot, do the work, give the slot back. Reading the GHSA details made it clear that once streaming (the consumer can leave halfway through) and async tasks (release can happen in a different execution context than acquire) are involved, "giving the slot back" isn't atomic at all — a slot's lifecycle splits into an acquire event and a release event that can happen on different tasks at different times, and an early interruption anywhere in between can leave the state stuck. It's the same class of problem as connection-pool or file-lock bugs, just wearing "AI agent concurrency control" as a costume.
References
- Pydantic AI v2.53.0 Release Notes
- Security Advisory GHSA-6fqq-452j-qhrp
- Pydantic AI GitHub
- Pydantic AI v2.52.0 — previous framework update
- PR #9478: Fix ConcurrencyLimitedModel cross-task release vulnerability
- PR #9041: Add ToolCallJudge
- PR #8370: Expose repair_messages as a public API
- PR #8946: Add AbsurdDurability
- PR #8942: Add SystemOneModel
- Full Changelog: v2.52.0...v2.53.0
Loading...