Choose Sync or Async

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

The Decision

The Anthropic SDK offers two clients with identical capabilities:

Anthropic (sync)AsyncAnthropic (async)
Calling styleclient.messages.create(...)await client.messages.create(...)
ConcurrencyOne request per thread at a timeMany requests per thread, cooperatively
Code complexitySimple, familiarRequires async/await throughout
FitsScripts, notebooks, sync frameworksAsync frameworks, high-concurrency services

Neither is "better". The right choice depends on where your code runs and how many things it does at once. This lesson gives you a practical way to decide, explains how to get concurrency without async, and shows how to combine both styles safely.


Start with the Environment

The most important question is what kind of code surrounds your Claude calls:

Your code runs in…UseWhy
A command-line script or toolSyncSimplest; concurrency rarely needed
A Jupyter notebook, one call at a timeSyncSimplest; async also works with top-level await
Flask, or traditional Django viewsSyncThese frameworks are synchronous; requests already run in separate threads
FastAPI, Starlette, aiohttp, or Django async viewsAsyncSync calls would block the event loop (Lessons 56 and 68)
Discord, Telegram, or WebSocket botsAsyncThese libraries run on an event loop
Celery or RQ background workersUsually syncWorkers run synchronous tasks; each worker processes one job at a time
Batch processing of many itemsAsync, threads, or the Batches APISee below

If the surrounding code is already async, use the async client. Mixing the sync client into async code blocks the event loop and slows down everything.


Then Consider Concurrency Needs

How many requests at once?Recommendation
One at a timeSync. Async adds complexity without benefit.
A few concurrent requests in otherwise sync codeSync client with a thread pool (see below)
Dozens or hundreds of concurrent requests or streamsAsync
Thousands of items that are not urgentMessage Batches API (Lessons 131–140)

Concurrency Without Async: Thread Pools

Synchronous code can run requests concurrently with threads. Python's concurrent.futures.ThreadPoolExecutor runs a function in several threads. While one thread waits for Claude, the others continue, which gives the same overlap of waiting time as async, without changing the rest of your code:

from concurrent.futures import ThreadPoolExecutor

from anthropic import Anthropic

client = Anthropic()   # One client, shared by all threads


def ask(question: str) -> str:
    message = client.messages.create(
        model="claude-haiku-5-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": question}],
    )
    return "".join(b.text for b in message.content if b.type == "text").strip()


with ThreadPoolExecutor(max_workers=5) as pool:
    answers = list(pool.map(ask, questions))   # Results in input order
  • max_workers=5 limits concurrency to five threads, like a semaphore.
  • pool.map() returns results in the same order as the inputs.
  • A single sync client can be shared across threads.

Threads vs. Async

Thread pool + sync clientAsync client
Changes to existing sync codeMinimalasync/await everywhere the calls flow
Practical concurrencyTens of requests (one thread each)Hundreds of requests or streams
Memory per concurrent requestHigher (a thread each)Lower (a coroutine each)
Cancellation and timeoutsHarder to stop a running threadBuilt in (cancel(), asyncio.timeout)
Best forAdding parallelism to sync applicationsAsync applications, high concurrency, many streams

Combining Sync and Async Code

Real projects sometimes need both styles. Use these bridges:

Calling Async Code from Sync Code

Start an event loop with asyncio.run(), once, at the boundary:

def summarize_all(texts: list[str]) -> list[str]:
    return asyncio.run(summarize_concurrently(texts))   # Sync function, async inside

Do not call asyncio.run() inside code that is already running in an event loop (Lesson 71).

Calling Sync Code from Async Code

Run blocking code in a thread with asyncio.to_thread(), so the event loop stays free (Lesson 74):

history = await asyncio.to_thread(load_history_from_sqlite, session_id)

Django: Sync and Async Views

Django supports both:

Django viewClaude client
def my_view(request):Anthropic
async def my_view(request): (with an ASGI server)AsyncAnthropic

In async Django views, database access must use Django's async ORM methods (such as aget() and acreate()) or sync_to_async(), so blocking calls do not freeze the event loop.


A Decision Checklist

  1. Is the surrounding framework async? Use AsyncAnthropic.
  2. Is the code synchronous, with one request at a time? Use Anthropic.
  3. Sync code that needs a little parallelism? Anthropic with a ThreadPoolExecutor.
  4. Hundreds of concurrent requests or streams? AsyncAnthropic, with a semaphore and retries (Lesson 75).
  5. Thousands of items, results not needed right away? The Message Batches API.
  6. Unsure? Start with sync. Moving to async later is a contained change, especially if your Claude logic lives in one module (Lesson 61).

Practical Example: The Same Job, Three Ways

This program answers the same six questions sequentially, with a thread pool, and with asyncio, using the same concurrency limit for the last two, and compares the times.

"""compare_approaches.py: sequential, thread pool, and asyncio versions of the same job."""

import asyncio
import time
from concurrent.futures import ThreadPoolExecutor

from anthropic import Anthropic, AsyncAnthropic

MODEL = "claude-haiku-5-5"
CONCURRENCY = 3
QUESTIONS = [
    "What is a Python wheel? One sentence.",
    "What is a lambda function? One sentence.",
    "What does 'if __name__ == \"__main__\"' do? One sentence.",
    "What is a list slice? One sentence.",
    "What is the GIL? One sentence.",
    "What is an f-string? One sentence.",
]


def text_of(message) -> str:
    return "".join(b.text for b in message.content if b.type == "text").strip()


def run_sequential() -> list[str]:
    client = Anthropic()
    return [text_of(client.messages.create(model=MODEL, max_tokens=1024,
                                           messages=[{"role": "user", "content": q}]))
            for q in QUESTIONS]


def run_threads() -> list[str]:
    client = Anthropic()

    def ask(question: str) -> str:
        return text_of(client.messages.create(model=MODEL, max_tokens=1024,
                                              messages=[{"role": "user", "content": question}]))

    with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
        return list(pool.map(ask, QUESTIONS))


async def run_async() -> list[str]:
    semaphore = asyncio.Semaphore(CONCURRENCY)
    async with AsyncAnthropic() as client:

        async def ask(question: str) -> str:
            async with semaphore:
                message = await client.messages.create(model=MODEL, max_tokens=1024,
                                                       messages=[{"role": "user", "content": question}])
            return text_of(message)

        return await asyncio.gather(*(ask(q) for q in QUESTIONS))


def timed(label: str, function) -> list[str]:
    start = time.perf_counter()
    answers = function()
    print(f"{label:<12} {time.perf_counter() - start:5.1f}s")
    return answers


if __name__ == "__main__":
    timed("sequential", run_sequential)
    timed("threads", run_threads)
    answers = timed("asyncio", lambda: asyncio.run(run_async()))
    print("\nFirst answer:", answers[0])

Example output (timings vary):

sequential    9.6s
threads       3.4s
asyncio       3.3s

First answer: A Python wheel is a built package format (.whl) that installs quickly without a build step.

How It Works

  1. run_sequential() sends one request at a time, so its time is the sum of all six.
  2. run_threads() shares one sync client across three worker threads. pool.map() runs ask for each question, at most three at a time, and returns answers in input order.
  3. run_async() uses one async client and a semaphore of three, with asyncio.gather() (Lessons 74 and 75). The ask coroutine is defined inside the async with block so it can use the client.
  4. timed() runs each approach and prints its duration. The async version is started with asyncio.run(), wrapped in a lambda so timed() can call it like the others.
  5. Results: with the same concurrency limit, threads and asyncio take about the same time, roughly a third of the sequential time with three-way concurrency. The difference between them appears at higher concurrency, where async scales with less overhead, and in async environments, where only async fits.

Common Mistakes

Using the Sync Client in Async Frameworks

What happens: each request blocks the event loop, and the server handles one user at a time.

Fix: use AsyncAnthropic in async code.

Rewriting a Sync Application in Async Without Need

What happens: large changes and new bugs, with no real benefit for one-request-at-a-time code.

Fix: keep sync code sync; add a thread pool if you need some parallelism.

Unlimited Threads or Coroutines

What happens: rate-limit errors and wasted resources.

Fix: limit concurrency with max_workers or a semaphore.

Calling asyncio.run() from Inside an Event Loop

What happens: RuntimeError: asyncio.run() cannot be called from a running event loop.

Fix: use await inside async code, and asyncio.run() only at the sync boundary.

Blocking Calls Hidden Inside Async Code

What happens: a synchronous database or file call inside a coroutine stalls all concurrent work.

Fix: use async libraries, or asyncio.to_thread().


Best Practices

  • Follow your environment: async frameworks need the async client; sync code is usually best with the sync client.
  • Default to sync for scripts, tools, and notebooks.
  • Use thread pools for modest parallelism in sync code.
  • Use async for high concurrency and many simultaneous streams.
  • Use the Batches API for large jobs that can wait.
  • Bridge carefully: asyncio.run() at sync boundaries, asyncio.to_thread() for blocking calls in async code.
  • Keep Claude logic in one module, so switching styles later is easy.

Exercise

  1. Run compare_approaches.py with CONCURRENCY set to 1, 3, and 6. How do the threads and asyncio times change?
  2. Use the decision checklist to choose a client for: a Django admin action that summarizes 20 records; a FastAPI chat endpoint; a nightly job that classifies 50,000 reviews; and a Jupyter notebook experiment.
  3. Rewrite run_threads() with concurrent.futures.as_completed() to print each answer as it finishes.
  4. Explain why max_workers in a ThreadPoolExecutor acts as a concurrency limit, similar to a semaphore.
Show answers for steps 2 and 4

Step 2:

  • Django admin action, 20 records: sync code (Django view), modest parallelism, so use the sync client with a ThreadPoolExecutor, or the async client in an async view.
  • FastAPI chat endpoint: an async framework, so use AsyncAnthropic (or the sync client in a def endpoint, Lesson 68).
  • Nightly job, 50,000 reviews: a large, non-urgent job, so use the Message Batches API.
  • Jupyter notebook experiment: the sync client for simplicity, unless you are experimenting with concurrency.

Step 4: a ThreadPoolExecutor creates at most max_workers threads. Each thread runs one task at a time, and additional tasks wait in the executor's internal queue until a thread becomes free. At most max_workers requests are therefore in flight at once, the same effect as a semaphore with that many slots.

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 Choose Sync or Async and get answers drawn from it.

Signed-in readers only.