Choosing Between Anthropic() and AsyncAnthropic() in Production Code
The anthropic package ships two client classes with an identical method surface: Anthropic() and AsyncAnthropic().
Search across all documentation pages
The anthropic package ships two client classes with an identical method surface: Anthropic() and AsyncAnthropic().
The right choice depends entirely on how your program handles concurrency, not on which one is "better."
Anthropic() is a blocking client - calling client.messages.create(...) pauses the current thread until a response arrives.
AsyncAnthropic() is a non-blocking client built on asyncio - calling await client.messages.create(...) yields control back to the event loop while the request is in flight.
Both expose the same parameters, the same response types, and the same error classes, so switching between them later is mostly mechanical.
The decision that matters is whether your application already runs on an event loop (an async web framework, an async job queue) or runs as a straight-line script, because that decision was effectively made for you before you ever picked a Claude client.
Getting this wrong doesn't break correctness, it costs throughput: a sync client called from inside an async framework blocks the whole event loop, and an async client used in a single-threaded script adds ceremony with no benefit.
import anthropic
# Script, notebook, CLI tool, batch job
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
# FastAPI, aiohttp, or any asyncio-based service
async_client = anthropic.AsyncAnthropic()
response = await async_client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)When to reach for Anthropic():
When to reach for AsyncAnthropic():
This example shows the same summarization endpoint written two ways: as a synchronous script and as a FastAPI route using the async client, so the contrast is direct.
# sync_script.py - a standalone batch job
import anthropic
client = anthropic.Anthropic()
def summarize(text: str) -> str:
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=300,
system="Summarize the input in one sentence.",
messages=[{"role": "user", "content": text}],
)
return message.content[0].text
if __name__ == "__main__":
documents = ["First document text...", "Second document text..."]
for doc in documents:
print(summarize(doc))# fastapi_service.py - an async web service
from fastapi import FastAPI
from pydantic import BaseModel
import anthropic
app = FastAPI()
client = anthropic.AsyncAnthropic()
class SummarizeRequest(BaseModel):
text: str
@app.post("/summarize")
async def summarize(req: SummarizeRequest):
message = await client.messages.create(
model="claude-sonnet-5",
max_tokens=300,
system="Summarize the input in one sentence.",
messages=[{"role": "user", "content": req.text}],
)
return {"summary": message.content[0].text}What this demonstrates:
messages.create) with the same parameters; only async/await and the client class differ.awaits the Claude call, so the event loop can serve other incoming requests while this one waits on the network.asyncio event loop runs one coroutine at a time but switches to another whenever the running one hits an await on I/O, such as a network call.AsyncAnthropic() builds its HTTP calls on top of httpx's async transport, so an outstanding Claude request yields control back to the loop instead of parking a thread.Anthropic() uses httpx's sync transport; a call blocks the calling thread until a response header arrives (or, when streaming, until the first chunk arrives).async def code still works, but it blocks the entire event loop for the duration of the call, stalling every other coroutine scheduled on that loop, including unrelated requests being served by the same FastAPI process.| Concern | Anthropic() | AsyncAnthropic() |
|---|---|---|
| Call style | client.messages.create(...) | await client.messages.create(...) |
| Blocks the calling thread while waiting | Yes | No - yields to the event loop |
| Natural fit | Scripts, CLIs, batch jobs, sync frameworks | FastAPI, aiohttp, any asyncio-based service |
| Concurrency model | One request at a time per thread (use a thread pool for more) | Many requests in flight on one event loop |
| Streaming context manager | with client.messages.stream(...) as stream: | async with client.messages.stream(...) as stream: |
# Running many summaries concurrently with the async client
import asyncio
import anthropic
client = anthropic.AsyncAnthropic()
async def summarize(text: str) -> str:
message = await client.messages.create(
model="claude-sonnet-5",
max_tokens=300,
messages=[{"role": "user", "content": text}],
)
return message.content[0].text
async def main():
documents = ["Doc A...", "Doc B...", "Doc C..."]
results = await asyncio.gather(*(summarize(doc) for doc in documents))
print(results)
asyncio.run(main())asyncio.gather is what actually earns you the concurrency benefit of the async client: it launches all the requests before awaiting any of them, so they run overlapped rather than one after another. Using AsyncAnthropic() but still awaiting each call sequentially in a loop gets you none of the throughput gain and only the added complexity.
client.messages.create(...) (no await) inside async def blocks the entire event loop for that call's duration, stalling every other request the process is handling. Fix: use AsyncAnthropic() inside async route handlers, or run the sync call in a thread pool with await run_in_threadpool(...).anthropic.Anthropic() or anthropic.AsyncAnthropic() inside a request handler discards connection pooling on every call. Fix: construct one client at module or app startup and reuse it.asyncio.run() calls inside an already-running event loop. Calling asyncio.run(...) from inside a FastAPI handler (which is already inside a loop) raises a RuntimeError. Fix: await the async client directly in the async handler rather than wrapping it in asyncio.run().await in a loop instead of asyncio.gather. for doc in docs: await summarize(doc) runs requests one at a time, giving up the concurrency the async client was chosen for. Fix: launch the coroutines together and await them as a group with asyncio.gather (or a bounded asyncio.Semaphore to cap concurrency).async with for async streaming. Using with instead of async with on AsyncAnthropic().messages.stream(...) raises a TypeError, since the async client's stream object is an async context manager. Fix: always pair AsyncAnthropic() streaming with async with and async for.AsyncAnthropic() takes the same wall-clock time as the same request through Anthropic() - the benefit only appears when multiple requests overlap. Fix: don't switch to async for latency reasons alone; switch it for concurrency.| Alternative | Use When | Don't Use When |
|---|---|---|
Anthropic() in a thread pool (run_in_threadpool) | An async framework needs to call a sync client without blocking the loop, e.g. reusing existing sync code | You're writing new code from scratch - just use AsyncAnthropic() directly |
AsyncAnthropic() everywhere, even in scripts | You want one consistent client type across a codebase that mixes scripts and services | The script has no concurrency to exploit - the asyncio.run() boilerplate adds noise for no gain |
Multiple Anthropic() clients across a thread pool | CPU-bound batch processing where each worker thread makes its own sequential calls | You need fine-grained control over how many requests are in flight at once - asyncio.Semaphore with the async client is more precise |
No.
Anthropic() and AsyncAnthropic() support the same parameters, the same streaming, and the same retry/timeout configuration; the only difference is blocking versus non-blocking calls.
You can, but calling it directly (without a thread pool) blocks the event loop for every other request being handled by that process during the call.
Prefer AsyncAnthropic() in async routes, or wrap the sync call in run_in_threadpool if you must reuse existing sync code.
The per-request latency is the same either way.
The added cost is code complexity: you need asyncio.run(), async def, and await for no throughput benefit if nothing runs concurrently.
Use asyncio.gather to launch multiple coroutines before awaiting them, rather than awaiting each one in sequence inside a loop. See the Python Notes section above.
Yes, in most production workloads.
Wrap each call with an asyncio.Semaphore sized to a sensible concurrency limit, so a large fan-out doesn't overwhelm your rate limits or the target service.
Modern Django supports async views, so AsyncAnthropic() works there too.
Classic synchronous Django views should use Anthropic() instead, matching the framework's execution model.
You get back a coroutine object instead of a Message, and any code that tries to read .content on it raises an AttributeError. Python does not run the coroutine's body until it is awaited.
No.
Both Anthropic() and AsyncAnthropic() are fully supported, generally available parts of the same anthropic package.
Mostly, yes.
Because both clients share method names and parameters, migrating usually means adding async/await, swapping the client class, and switching with to async with for streaming - not redesigning the calls themselves.
The streaming context manager exists on both, but the async version is an async context manager iterated with async for instead of for. The underlying stream of events is otherwise the same.
Celery workers are typically synchronous, one task per worker process/thread, so Anthropic() matches that execution model unless you're running Celery with an async pool.
stream() on both client typesAsyncAnthropic with tool usemax_retries and timeout on both clientsStack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 19, 2026