Skip to content

fix: use async_maybe_transform in AsyncMessages.stream() to avoid blocking event loop#1231

Open
MaxwellCalkin wants to merge 1 commit intoanthropics:mainfrom
MaxwellCalkin:fix/async-maybe-transform-in-stream
Open

fix: use async_maybe_transform in AsyncMessages.stream() to avoid blocking event loop#1231
MaxwellCalkin wants to merge 1 commit intoanthropics:mainfrom
MaxwellCalkin:fix/async-maybe-transform-in-stream

Conversation

@MaxwellCalkin
Copy link

Summary

Fixes #1195

AsyncMessages.stream() calls the synchronous maybe_transform() instead of async_maybe_transform(), causing _transform_recursive to block the event loop. This is especially impactful for large message payloads in async contexts (e.g., FastAPI gateways), where profiling shows ~5-8% CPU time spent in the blocking recursive walk.

Changes

  • src/anthropic/resources/messages/messages.pyAsyncMessages.stream(): Wrapped the _post call in an inner async def make_request() coroutine that uses await async_maybe_transform(). The coroutine object is passed to AsyncMessageStreamManager (which accepts any Awaitable), so the public API is unchanged — callers still use async with client.messages.stream(...) as stream: without needing await.

  • src/anthropic/resources/beta/messages/messages.py — Same fix for AsyncMessages.stream(), plus a fix for AsyncMessages.parse() which had the same sync maybe_transform issue.

Why the inner coroutine approach?

The stream() method is intentionally a non-async def that returns an AsyncMessageStreamManager. Making it async def would break the public API (users would need async with await client.messages.stream(...) instead of async with client.messages.stream(...)). By wrapping the transform + post in an inner async def make_request(), we defer both the transform and the HTTP call to the __aenter__ phase of the context manager, keeping the API unchanged while ensuring the transform runs asynchronously.

Note for maintainers

This is Stainless-generated code. The fix should be ported to the codegen template so it persists across regenerations. Specifically, the stream helper template for async classes should use async_maybe_transform instead of maybe_transform.

Test plan

  • Verify async with client.messages.stream(...) still works without await
  • Verify the transform runs on the event loop (not blocking) by profiling with large payloads
  • Run existing test suite to confirm no regressions

…cking event loop

In `AsyncMessages.stream()`, the synchronous `maybe_transform()` was called
instead of the async variant, causing `_transform_recursive` to block the
event loop. This is especially impactful for large message payloads in
async contexts (e.g., FastAPI gateways).

The fix wraps the `_post` call in an inner `async def make_request()`
coroutine that uses `await async_maybe_transform()`. The coroutine object
is passed to `AsyncMessageStreamManager` (which accepts any `Awaitable`),
so the public API is unchanged — callers still use
`async with client.messages.stream(...) as stream:` without `await`.

Also fixes the same issue in `beta/messages/messages.py` for both
`AsyncMessages.stream()` and `AsyncMessages.parse()`.

Fixes anthropics#1195

Note: This is Stainless-generated code. The fix should be ported to the
codegen template so it persists across regenerations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@MaxwellCalkin MaxwellCalkin requested a review from a team as a code owner March 8, 2026 20:10
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.

Python SDK: _transform_recursive blocking event loop on large message payloads

1 participant