Choosing Between Anthropic() and AsyncAnthropic() in Production Code
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."
Summary
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.
Recipe
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():
- One-off scripts, notebooks, and CLI tools that make requests sequentially.
- Batch or ETL jobs that process items one at a time and don't need overlap between requests.
- Any codebase built on a synchronous web framework (Flask or Django without async views).
When to reach for AsyncAnthropic():
- Web services built on an async framework, most commonly FastAPI, that already run on an event loop.
- Workloads that fan out many Claude calls concurrently (summarizing 50 documents at once, for example).
- Any code that needs to call Claude while also awaiting other I/O (a database query, another API call) without blocking either.
Working Example
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:
- Both versions call the same method (
messages.create) with the same parameters; onlyasync/awaitand the client class differ. - The FastAPI route
awaits the Claude call, so the event loop can serve other incoming requests while this one waits on the network. - Both clients are constructed once at module scope and reused across calls, not recreated per request.
- The batch script has no concurrent requests to overlap, so the sync client adds no cost there.
Deep Dive
How It Works
- Python's
asyncioevent loop runs one coroutine at a time but switches to another whenever the running one hits anawaiton I/O, such as a network call. AsyncAnthropic()builds its HTTP calls on top ofhttpx's async transport, so an outstanding Claude request yields control back to the loop instead of parking a thread.Anthropic()useshttpx's sync transport; a call blocks the calling thread until a response header arrives (or, when streaming, until the first chunk arrives).- A sync client called from inside
async defcode 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. - Constructing a fresh client per request (sync or async) discards connection pooling and forces a new TLS handshake more often than necessary; both client classes are meant to be built once and reused.
Sync vs Async at a Glance
| 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: |
Python Notes
# 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.
Gotchas
- Calling the sync client inside an async FastAPI route.
client.messages.create(...)(noawait) insideasync defblocks the entire event loop for that call's duration, stalling every other request the process is handling. Fix: useAsyncAnthropic()inside async route handlers, or run the sync call in a thread pool withawait run_in_threadpool(...). - Constructing a new client per request. Building
anthropic.Anthropic()oranthropic.AsyncAnthropic()inside a request handler discards connection pooling on every call. Fix: construct one client at module or app startup and reuse it. - Mixing
asyncio.run()calls inside an already-running event loop. Callingasyncio.run(...)from inside a FastAPI handler (which is already inside a loop) raises aRuntimeError. Fix:awaitthe async client directly in the async handler rather than wrapping it inasyncio.run(). - Sequential
awaitin a loop instead ofasyncio.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 withasyncio.gather(or a boundedasyncio.Semaphoreto cap concurrency). - Forgetting
async withfor async streaming. Usingwithinstead ofasync withonAsyncAnthropic().messages.stream(...)raises aTypeError, since the async client's stream object is an async context manager. Fix: always pairAsyncAnthropic()streaming withasync withandasync for. - Assuming the async client is faster per call. A single request through
AsyncAnthropic()takes the same wall-clock time as the same request throughAnthropic()- the benefit only appears when multiple requests overlap. Fix: don't switch to async for latency reasons alone; switch it for concurrency.
Alternatives
| 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 |
FAQs
Do I lose any SDK features by choosing the sync client over the async one?
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.
Can I use the sync client inside a FastAPI route at all?
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.
Is there a performance cost to using AsyncAnthropic() in a script with no concurrency?
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.
How do I run several Claude calls at the same time with the async client?
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.
Should I cap how many requests run concurrently?
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.
Does Django work with AsyncAnthropic()?
Modern Django supports async views, so AsyncAnthropic() works there too.
Classic synchronous Django views should use Anthropic() instead, matching the framework's execution model.
What happens if I forget "await" on an AsyncAnthropic() call?
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.
Is the async client considered beta or experimental?
No.
Both Anthropic() and AsyncAnthropic() are fully supported, generally available parts of the same anthropic package.
Can I switch a codebase from sync to async later without a rewrite?
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.
Does streaming behave differently between the two clients?
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.
Which client should a Celery worker use?
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.
Related
- The Anthropic Python SDK Mental Model - why sync and async share one API surface
- Streaming Responses with the Python SDK -
stream()on both client types - Handling Streaming Tool Use in Async Python Workflows - combining
AsyncAnthropicwith tool use - Python SDK Retry and Timeout Configuration Reference -
max_retriesandtimeouton both clients
Stack 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.