Close Async Clients

Su Suriya Ravichandran Updated 10 Nov 2026
8 min read ·Lesson 79 of 200

Why Async Clients Must Be Closed

An AsyncAnthropic client keeps a pool of open network connections so that requests can reuse them (Lesson 72). Those connections hold resources: sockets, memory, and, with the aiohttp backend, an HTTP session object. Closing the client releases them.

If async clients are not closed:

ProblemSymptom
Leaked connectionsResource usage grows in long-running services
Warnings at exitMessages such as "Unclosed client session" (aiohttp backend) or unclosed-transport warnings
Errors at shutdownConnections cleaned up after the event loop has closed can raise errors
Dirty testsEach test leaves connections open, causing warnings and flaky behavior

The rule is simple: the code that creates a client closes it, exactly once, when the client is no longer needed.


Three Ways to Close

async with AsyncAnthropic() as client:
    ...
# Closed here, even if an exception occurred

async with calls the client's cleanup when the block exits, normally or because of an exception or cancellation.

2. await client.close()

When the client must live outside a single block, close it explicitly, in a finally block so it always runs:

client = AsyncAnthropic()
try:
    await run_application(client)
finally:
    await client.close()

close() is a coroutine on the async client, so it must be awaited. Calling client.close() without await creates a coroutine that never runs, and the client stays open.

3. Framework Lifecycle Hooks

In web frameworks, create the client at startup and close it at shutdown (Lesson 68):

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

Closing Streams vs. Closing Clients

These are two different levels:

Closing a streamClosing a client
What it endsOne responseAll connections of the client
WhenWhen a response is complete or no longer needed (Lesson 60)When the application, or component, is finished with Claude
HowLeave async with client.messages.stream(...)Leave async with AsyncAnthropic(), or await client.close()
FrequencyEvery requestOnce per client lifetime

Close streams often, after each response. Close the client rarely, at the end of its life. Closing the client after every request would throw away the connection pool and slow down every request.


Order of Shutdown

Close things in the reverse order of their dependencies:

1. Stop accepting new work
2. Let in-progress work finish (with a time limit), or cancel it
3. Wait for all tasks to finish
4. Close the Claude client
5. Close other resources (database connections, files)

Closing the client before tasks are done would make those tasks fail with connection errors in the middle of their work. Using the client inside async with, and awaiting all tasks inside that block, gets the order right automatically.


Closing Clients in Tests

Tests create clients too, and should close them. With pytest and the pytest-asyncio plugin, a fixture can create and close a client for each test:

import pytest_asyncio
from anthropic import AsyncAnthropic


@pytest_asyncio.fixture
async def claude():
    async with AsyncAnthropic() as client:
        yield client

The code after yield, here the end of the async with block, runs after each test that uses the fixture, so every test starts with a fresh client and leaves nothing open. Lessons 181–184 show how to test without real API calls.


Practical Example: A Worker Service with Graceful Shutdown

This long-running service processes incoming questions with async workers. When it receives a stop signal (Ctrl+C, or SIGTERM from a process manager such as systemd or Docker), it shuts down gracefully: it stops taking new work, lets in-progress work finish within a time limit, stops the workers, and closes the Claude client.

"""worker_service.py: a long-running async service with graceful shutdown."""

import asyncio
import itertools
import signal

import anthropic
from anthropic import AsyncAnthropic

WORKER_COUNT = 2
SHUTDOWN_GRACE_SECONDS = 30
SAMPLE_QUESTIONS = itertools.cycle([
    "What is a Python iterator? One sentence.",
    "What is type hinting? One sentence.",
    "What is a context manager? One sentence.",
])


async def produce(queue: asyncio.Queue, stop: asyncio.Event) -> None:
    """Simulate incoming work: one new question every 2 seconds, until stopped."""
    number = 0
    while not stop.is_set():
        number += 1
        await queue.put((number, next(SAMPLE_QUESTIONS)))
        await asyncio.sleep(2)


async def work(name: str, client: AsyncAnthropic, queue: asyncio.Queue) -> None:
    while True:
        number, question = await queue.get()
        try:
            message = await client.messages.create(
                model="claude-haiku-5-5",
                max_tokens=1024,
                messages=[{"role": "user", "content": question}],
            )
            text = "".join(b.text for b in message.content if b.type == "text").strip()
            print(f"[{name}] #{number}: {text[:60]}")
        except anthropic.APIError as error:
            print(f"[{name}] #{number} failed: {type(error).__name__}")
        finally:
            queue.task_done()


async def main() -> None:
    stop = asyncio.Event()
    loop = asyncio.get_running_loop()
    for sig in (signal.SIGINT, signal.SIGTERM):
        try:
            loop.add_signal_handler(sig, stop.set)
        except NotImplementedError:
            pass   # Windows: Ctrl+C raises KeyboardInterrupt instead (see below)

    queue: asyncio.Queue = asyncio.Queue()
    async with AsyncAnthropic() as client:
        workers = [asyncio.create_task(work(f"worker-{n}", client, queue))
                   for n in range(1, WORKER_COUNT + 1)]
        producer = asyncio.create_task(produce(queue, stop))
        print("Service running. Press Ctrl+C to stop.")

        try:
            await stop.wait()
        finally:
            print("\nStopping: no new work accepted.")
            producer.cancel()
            try:
                await asyncio.wait_for(queue.join(), timeout=SHUTDOWN_GRACE_SECONDS)
                print("All in-progress work finished.")
            except asyncio.TimeoutError:
                print("Grace period over; cancelling remaining work.")
            for task in workers:
                task.cancel()
            await asyncio.gather(producer, *workers, return_exceptions=True)

    print("Claude client closed. Goodbye.")


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("Stopped.")

Example output:

Service running. Press Ctrl+C to stop.
[worker-1] #1: A Python iterator is an object that returns items one at a ti
[worker-2] #2: Type hinting adds optional type annotations to Python code to
[worker-1] #3: A context manager sets up and cleans up a resource around a b
^C
Stopping: no new work accepted.
All in-progress work finished.
Claude client closed. Goodbye.

How It Works

  1. stop is an asyncio.Event, a flag that coroutines can wait for. The signal handlers registered with loop.add_signal_handler() set it when Ctrl+C (SIGINT) or SIGTERM arrives, instead of interrupting the program abruptly.
  2. produce() simulates incoming work. It stops adding questions as soon as stop is set.
  3. Workers take questions from the queue and process them, as in Lesson 74. task_done() in finally marks each item as finished, even on errors.
  4. await stop.wait() keeps main() waiting while the service runs. The finally block that follows performs the shutdown in the correct order:
    • stop new work by cancelling the producer,
    • let current work finish with queue.join(), limited to a grace period with wait_for(),
    • cancel the workers, which are otherwise waiting forever for new items, and wait for all tasks with gather(..., return_exceptions=True).
  5. The client is closed last, when the async with AsyncAnthropic() block ends, which happens only after every task has finished. No task can use a closed client.
  6. On Windows, add_signal_handler() is not available and raises NotImplementedError, so Ctrl+C raises KeyboardInterrupt instead. asyncio.run() then cancels main(). The finally block and the async with still run their cleanup, although the grace period may be cut short, and the outer except KeyboardInterrupt prints a final message.

Common Mistakes

Calling close() Without await

What happens: the close coroutine never runs, and the client stays open. Python warns that the coroutine was never awaited.

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

Closing the Client While Tasks Still Use It

What happens: in-progress requests fail with connection errors during shutdown.

Fix: finish or cancel all tasks first, then close the client.

Closing the Client After Every Request

What happens: the connection pool is useless, and every request pays for a new connection.

Fix: close streams after each response, and the client once at the end.

Library Code Closing a Shared Client

What happens: other components that share the client fail.

Fix: only the code that created the client closes it (Lesson 72).

No Shutdown Plan for Services

What happens: stopping the service abandons requests midway, and leaves resources open.

Fix: handle stop signals, finish or cancel work within a grace period, then close resources.


Best Practices

  • Prefer async with AsyncAnthropic() so closing is automatic.
  • Use await client.close() in finally when async with is not possible.
  • Tie client lifetimes to application lifetimes in web frameworks.
  • Shut down in order: stop new work, finish or cancel tasks, then close the client.
  • Close streams per response, and clients once.
  • Close clients in tests with fixtures.

Exercise

  1. Remove async with from a small script, never close the client, and run it with the aiohttp backend (Lesson 72). What warnings appear?
  2. Change SHUTDOWN_GRACE_SECONDS to 1, press Ctrl+C while a request is in progress, and observe which message appears.
  3. Rewrite main() to create the client with client = AsyncAnthropic() and close it with await client.close() in a finally block, keeping the same shutdown order.
  4. Explain why the async with AsyncAnthropic() block in main() must contain the whole shutdown sequence, rather than ending before it.
Show answers for steps 3 and 4

Step 3 (structure only):

client = AsyncAnthropic()
try:
    workers = [...]
    producer = ...
    try:
        await stop.wait()
    finally:
        ...   # Same shutdown steps: cancel producer, join queue, cancel workers, gather
finally:
    await client.close()

The inner finally performs the shutdown of tasks; the outer finally closes the client afterward. The order is identical to the async with version.

Step 4: if the async with block ended before the shutdown sequence, the client would be closed while workers were still processing items during the grace period. Their requests would fail with connection errors. Keeping the whole shutdown inside the block guarantees that the client is closed only after every task has finished.

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 Close Async Clients and get answers drawn from it.

Signed-in readers only.