Create an Async Client

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 72 of 200

The AsyncAnthropic Class

The asynchronous client is created like the synchronous one:

from anthropic import AsyncAnthropic

client = AsyncAnthropic()

It reads the same environment variables (ANTHROPIC_API_KEY first, Lesson 7), accepts the same constructor options, and has the same resources: client.messages, client.models, client.beta, and so on. The difference is that its methods return coroutines that you await.

OptionExampleSame as sync (Lesson 10)?
api_keyAsyncAnthropic(api_key=load_secret(...))Yes
timeoutAsyncAnthropic(timeout=60.0)Yes
max_retriesAsyncAnthropic(max_retries=4)Yes
base_urlAsyncAnthropic(base_url="https://gateway.example.com")Yes
default_headersAsyncAnthropic(default_headers={"anthropic-workspace-id": "wrkspc_..."})Yes
http_clientAsyncAnthropic(http_client=DefaultAioHttpClient())Async-specific HTTP clients

Like the sync client, creating an AsyncAnthropic object does not contact the API. Network activity starts with the first request.


One Client, Many Requests

An async client manages a connection pool: open HTTPS connections reused across requests. Concurrent requests share the pool, so one client per application is the right default:

client = AsyncAnthropic()

answers = await asyncio.gather(
    client.messages.create(...),   # All three share the same client and its connections
    client.messages.create(...),
    client.messages.create(...),
)

Creating a new client for each request throws away the pool and adds connection setup time to every call.


The Event-Loop Rule

Async connections belong to the event loop that was running when they were opened. That leads to one important rule:

Use an async client within a single event loop. Do not reuse the same client across separate asyncio.run() calls.

client = AsyncAnthropic()

asyncio.run(task_one(client))   # Opens connections on event loop #1, then closes loop #1
asyncio.run(task_two(client))   # Event loop #2 tries to reuse connections from the closed loop #1

The second call can fail with errors such as RuntimeError: Event loop is closed, because pooled connections from the first loop are still referenced.

Fixes:

  • Call asyncio.run() once, with one top-level main() coroutine that does all the work, or
  • create the client inside the coroutine that runs on the loop, for example with async with AsyncAnthropic() as client: inside main().

Web frameworks such as FastAPI run a single event loop for the whole application, so a client created at startup (in the lifespan, Lesson 68) is safe for every request.


Client Lifecycle Patterns

Pattern 1: Scripts, with async with

async def main() -> None:
    async with AsyncAnthropic() as client:
        ...   # All requests here

asyncio.run(main())

async with creates the client inside the running loop and closes its connections when the block ends (Lesson 79).

Pattern 2: Applications, Created at Startup

@asynccontextmanager
async def lifespan(app):
    app.state.claude = AsyncAnthropic()
    yield
    await app.state.claude.close()

The client lives for the whole application, on the application's event loop, and is closed cleanly at shutdown.

Pattern 3: Libraries, Client Passed In

class Summarizer:
    def __init__(self, client: AsyncAnthropic):
        self.client = client

Reusable code accepts a client instead of creating one. The application controls creation, configuration, and closing, and tests can pass a fake client (Lessons 181–182).


The aiohttp Backend

By default, the async client uses httpx2 for HTTP. The SDK also supports aiohttp as an alternative backend, which the SDK documentation recommends for improved concurrency performance:

python -m pip install "anthropic[aiohttp]"
from anthropic import AsyncAnthropic, DefaultAioHttpClient


async def main() -> None:
    async with AsyncAnthropic(http_client=DefaultAioHttpClient()) as client:
        message = await client.messages.create(
            model="claude-opus-5-5",
            max_tokens=1024,
            messages=[{"role": "user", "content": "Hello!"}],
        )
  • DefaultAioHttpClient() is an HTTP client configured with the SDK's defaults, using aiohttp.
  • Your code does not change otherwise: requests, streaming, and errors work the same way.
  • Consider it for applications with many concurrent requests or streams. For occasional requests, the default backend is fine.

Custom HTTP Settings

To customize the default httpx2-based backend, for example to add a proxy, while keeping the SDK's defaults, use DefaultAsyncHttpxClient:

from anthropic import AsyncAnthropic, DefaultAsyncHttpxClient

client = AsyncAnthropic(http_client=DefaultAsyncHttpxClient(proxy="http://proxy.internal:8080"))

Per-Request Options

with_options() works exactly as on the sync client (Lesson 10). It returns a copy of the client with changed settings, sharing the same connection pool:

fast = client.with_options(timeout=15.0, max_retries=1)
message = await fast.messages.create(...)

Practical Example: A Shared Async Client Module

This module provides one correctly managed async client for a whole program: configured from environment variables, created inside the running event loop, and closed once at the end. Other modules receive the client instead of creating their own.

claude_runtime.py

"""claude_runtime.py: create and manage the application's async Claude client."""

import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from anthropic import AsyncAnthropic, DefaultAioHttpClient


@asynccontextmanager
async def claude_client() -> AsyncIterator[AsyncAnthropic]:
    """Create the shared async client inside the running event loop, and close it at the end."""
    options = {
        "timeout": float(os.environ.get("CLAUDE_TIMEOUT", "60")),
        "max_retries": int(os.environ.get("CLAUDE_MAX_RETRIES", "2")),
    }
    if os.environ.get("CLAUDE_HTTP_BACKEND") == "aiohttp":
        options["http_client"] = DefaultAioHttpClient()

    async with AsyncAnthropic(**options) as client:
        yield client

summarizer.py

"""summarizer.py: reusable logic that receives a client instead of creating one."""

from anthropic import AsyncAnthropic


class Summarizer:
    def __init__(self, client: AsyncAnthropic, model: str = "claude-opus-5-5"):
        self.client = client
        self.model = model

    async def summarize(self, text: str, words: int = 30) -> str:
        message = await self.client.messages.create(
            model=self.model,
            max_tokens=1024,
            messages=[{"role": "user", "content":
                       f"<text>\n{text}\n</text>\n\nSummarize the text above in at most {words} words."}],
        )
        return "".join(b.text for b in message.content if b.type == "text").strip()

main.py

"""main.py: one event loop, one client, shared by all components."""

import asyncio

from claude_runtime import claude_client
from summarizer import Summarizer

ARTICLES = [
    "FastAPI is a modern Python web framework that uses type hints for validation and documentation...",
    "SQLite is a self-contained, serverless database engine stored in a single file...",
]


async def main() -> None:
    async with claude_client() as client:
        summarizer = Summarizer(client)
        summaries = await asyncio.gather(*(summarizer.summarize(text) for text in ARTICLES))
    for summary in summaries:
        print("-", summary)


if __name__ == "__main__":
    asyncio.run(main())

Example output:

- FastAPI is a modern Python web framework that uses type hints to validate data and generate API documentation automatically.
- SQLite is a lightweight, serverless database that stores an entire database in a single file.

How It Works

  1. claude_client() is an async context manager, written with @asynccontextmanager. Code before yield runs when the async with block starts, and the cleanup runs when it ends. Here, the cleanup is handled by the inner async with AsyncAnthropic(...), which closes the client.
  2. Configuration comes from environment variables (Lesson 8): timeout, retries, and optionally the aiohttp backend through CLAUDE_HTTP_BACKEND=aiohttp. The defaults apply when they are not set.
  3. The client is created inside main(), which runs on the event loop started by asyncio.run(). That follows the event-loop rule.
  4. Summarizer receives the client in its constructor. It never creates or closes clients, so it can be reused anywhere, and tested with a fake client.
  5. asyncio.gather(*(...)) runs both summaries concurrently with the shared client. The generator expression creates one coroutine per article.
  6. asyncio.run(main()) is called once, so there is exactly one event loop for the program's lifetime.

Common Mistakes

Reusing a Client Across asyncio.run() Calls

What happens: Event loop is closed errors, or hangs, on later calls.

Fix: one asyncio.run(main()) per program, with the client created inside it.

Creating a Client per Request

What happens: slower requests and wasted resources.

Fix: share one client, and use with_options() for per-request variations.

Never Closing the Client

What happens: warnings about unclosed connections, and leaked resources in long-running services.

Fix: use async with, or await client.close() at shutdown.

Creating Clients Deep Inside Library Code

What happens: components cannot share connections, and tests cannot replace the client.

Fix: pass the client in from the application.

Using the aiohttp Backend Without the Extra

What happens: an import error, because aiohttp is not installed.

Fix: install anthropic[aiohttp].


Best Practices

  • Create one async client per application (per credential), on the application's event loop.
  • Manage its lifetime with async with, or startup and shutdown hooks.
  • Pass the client into components instead of creating clients inside them.
  • Configure it from the environment: timeout, retries, and backend.
  • Consider the aiohttp backend for high concurrency.
  • Use with_options() for per-request settings.

Exercise

  1. Write a script that calls asyncio.run() twice with the same client, and observe the behavior. Then fix it by using a single main().
  2. Run main.py with CLAUDE_HTTP_BACKEND=aiohttp (after installing the extra) and confirm that it works the same way.
  3. Add a translate(text, language) method to Summarizer, and run a summary and a translation concurrently.
  4. Explain why Summarizer should not call client.close().
Show answers for steps 1 and 4

Step 1: the fixed version moves both tasks into one coroutine:

async def main() -> None:
    async with AsyncAnthropic() as client:
        await task_one(client)
        await task_two(client)

asyncio.run(main())

Both tasks run on the same event loop, and the client is created and closed on that loop.

Step 4: Summarizer does not own the client. It received it from the application, which may share the same client with other components. If Summarizer closed it, every other component using that client would fail. The code that creates a resource is responsible for closing it.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Create an Async Client and get answers drawn from it.

Signed-in readers only.