← Blog

Claude API Async Request Handling in Python

2026-10-13 · 4 min read · SubToAPI Team

Handling Claude API requests asynchronously in Python means using asyncio together with an async-capable HTTP client — either the official SDK's AsyncAnthropic class or a library like httpx — so your program can fire off multiple requests concurrently instead of waiting for each one to finish before starting the next.

This matters because LLM calls are I/O-bound and often slow: a single Claude response can take several seconds, and if you're processing a batch of documents, chat sessions, or background jobs synchronously, you're wasting most of that time just waiting on the network. Async handling lets you run dozens of requests in parallel, respect rate limits with controlled concurrency, and stream tokens back to users without blocking your event loop.

Why Synchronous Calls Don't Scale

A typical synchronous script looks like this:

import anthropic

client = anthropic.Anthropic(api_key="sk-ant-...")

for prompt in prompts:
    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=512,
        messages=[{"role": "user", "content": prompt}],
    )
    print(response.content[0].text)

If each call takes 3 seconds and you have 100 prompts, this takes roughly 5 minutes. With async execution and reasonable concurrency, the same batch can finish in a fraction of that time, bounded mostly by your rate limits rather than total latency.

Using the Async SDK Client

The official Python SDK ships an async client that mirrors the sync one but uses await:

import asyncio
from anthropic import AsyncAnthropic

client = AsyncAnthropic(api_key="sk-ant-...")

async def ask(prompt):
    response = await client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    )
    return response.content[0].text

result = asyncio.run(ask("Explain asyncio in one paragraph"))
print(result)

This is the easiest path if you're already committed to the official SDK and just need non-blocking calls inside an async web framework like FastAPI.

Running Requests Concurrently with a Semaphore

The real benefit of async shows up when you batch multiple prompts. Use asyncio.gather to run tasks in parallel, and a Semaphore to cap concurrency so you don't blow through your rate limits:

import asyncio
from anthropic import AsyncAnthropic

client = AsyncAnthropic(api_key="sk-ant-...")
sem = asyncio.Semaphore(5)  # max 5 concurrent requests

async def ask(prompt):
    async with sem:
        response = await client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=512,
            messages=[{"role": "user", "content": prompt}],
        )
        return response.content[0].text

async def main(prompts):
    tasks = [ask(p) for p in prompts]
    return await asyncio.gather(*tasks, return_exceptions=True)

results = asyncio.run(main(["Summarize A", "Translate B", "Classify C"]))

return_exceptions=True is important — without it, one failed request cancels the entire gather call and you lose the rest of your results. Check each item in results for an Exception instance before using it.

Async Streaming for Real-Time Output

If you're building a chat UI or CLI tool, you usually want tokens streamed as they're generated rather than waiting for the full response:

async def stream_answer(prompt):
    async with client.messages.stream(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    ) as stream:
        async for text in stream.text_stream:
            print(text, end="", flush=True)

asyncio.run(stream_answer("Write a haiku about concurrency"))

This pattern works the same whether you're on the official API or a wrapper — the key is that the streaming context manager yields chunks asynchronously without blocking your event loop.

Doing It with Raw HTTP (httpx)

If you'd rather not depend on the SDK — or you're hitting a Claude-compatible endpoint directly — httpx.AsyncClient works just as well:

import asyncio
import httpx

API_KEY = "sub_live_..."

async def ask(client, prompt):
    resp = await client.post(
        "https://api.subtoapi.app/v1/messages",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "model": "claude-sonnet-4-5",
            "max_tokens": 512,
            "messages": [{"role": "user", "content": prompt}],
        },
    )
    return resp.json()

async def main(prompts):
    async with httpx.AsyncClient(timeout=60) as client:
        return await asyncio.gather(*(ask(client, p) for p in prompts))

results = asyncio.run(main(["Prompt 1", "Prompt 2", "Prompt 3"]))

This same shape works for SubToAPI, which exposes Claude through a standard HTTPS endpoint with sub_live_... application keys — the async code above doesn't change whether you're calling Anthropic directly or through a proxy that adds usage metadata and team-level keys. See the messages reference for the full request schema, streaming docs for server-sent events, and tool use docs if your async requests also need function calling.

Handling Retries and Backoff

Async code still needs retry logic for rate limits (429) and transient server errors (5xx). A simple exponential backoff wrapper:

import asyncio
import random

async def with_retry(coro_fn, *args, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            return await coro_fn(*args)
        except Exception as e:
            if attempt == max_attempts - 1:
                raise
            wait = (2 ** attempt) + random.random()
            await asyncio.sleep(wait)

Wrap your ask calls with this instead of calling them bare inside gather, so a single rate-limit hiccup doesn't fail the whole batch.

Practical Tips

Questions

Does async make individual Claude responses faster? No. Async doesn't reduce the latency of a single request — it lets you run many requests concurrently so your total wall-clock time for a batch drops significantly.

What's the difference between asyncio.gather and a task queue? gather is fine for small, fixed batches run once. For long-running or continuous workloads, a bounded queue with worker coroutines gives you better control over backpressure and retry handling.

Can I use async with streaming and tool use together? Yes — the async streaming context manager works the same way whether the response includes plain text or tool-use blocks; you just need to handle both event types inside the async for loop. See /docs/tools for details.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →