Skip to content

Framework Update: Pydantic AI v2.53.0

Oct 3, 20261 min
TL;DRThree things in Pydantic AI v2.53.0: (1) security — GHSA-6fqq-452j-qhrp (high): a streamed request through ConcurrencyLimitedModel or limit_model_concurrency could keep its concurrency slot stuck in a 'released but not really released' state when the consumer exited early (stopped iterating, raised, or was cancelled) or fully consumed stream_text() with its default debouncing; repeating this could block every request sharing the limiter; (2) the fix also changes limiter semantics — a model wrapper now raises UserError when it shares a limiter with the agent making the request or an enclosing wrapper, ConcurrencyLimiter.acquire() takes a new slot on every call (even within the same task), and a custom AbstractConcurrencyLimiter must allow release() from another task; (3) a new ToolCallJudge reviews tool calls before they execute, and the internal repair_messages pipeline is now a public API.

🌏 中文版

Release Info

ItemValue
FrameworkPydantic AI
Versionv2.53.0
Previousv2.52.0
Released2026-10-01
Release NotesGitHub Release
Security AdvisoryGHSA-6fqq-452j-qhrp
GitHubpydantic/pydantic-ai
Stars20.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

  • ConcurrencyLimiter semantics changed: acquire() now takes a new slot on every call, even within the same task, and a custom AbstractConcurrencyLimiter must now allow release() 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 UserError when 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 mechanism
  • repair_messages goes 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 it
  • AbsurdDurability (#8946): added to the harness, replacing the previously independent pydantic-ai-absurd → one more durable-execution backend folded back into the main repo
  • SystemOneModel (#8942): lets decision models like CLM and Laya run through the /v1/systemone API → fills in a call shape for fast decision/classification models, distinct from ordinary chat models
  • pydantic-clai2's plugin ecosystem keeps growing: this release adds built-in posthog, grain, linear, and herdr plugins, plus a Logfire telemetry settings menu, /update with 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 AbstractConcurrencyLimiter must support release() from another task
    • Affected: projects with a custom AbstractConcurrencyLimiter subclass, or any code relying on the old slot-accounting details
  • 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 ConcurrencyLimiter instance into both the agent and a model wrapper will now error at startup or runtime, and needs to split into separate limiter instances

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