When to Use Async vs Sync
On this page
The Choice Isn’t Always Obvious
Async isn’t automatically superior to sync. Each approach has contexts where it excels, and choosing the wrong one can complicate your code without performance gains. This lesson helps you decide which approach to adopt for your situation.
When Sync Is Enough
The sync client remains the right choice more often than you might think, and the reason is always the same: when parallelism adds nothing, its complexity is a net cost. A one-off test or exploration script sending a few requests gains nothing from async—code simplicity comes first, and you will read this script again in six months. A sequential pipeline where each request depends on the previous result cannot parallelize anything anyway: async would add syntax without changing execution time by a second. Same logic for a traditional Django application: if your stack isn’t async, adding asyncio for API calls alone creates a sync/async boundary that is painful to maintain. And in the prototyping phase, when you’re testing a prompt or validating a concept, sync code is faster to read and write—exactly what you want from a prototype.
# Sync — perfect for a sequential pipeline
from openai import OpenAI
client = OpenAI(api_key=os.getenv("XAI_API_KEY"), base_url="https://api.x.ai/v1")
# Step 1: analyze the text
analysis = client.chat.completions.create(
model="grok-4.5",
messages=[{"role": "user", "content": f"Analyze this text: {text}"}]
).choices[0].message.content
# Step 2: generate a summary based on the analysis
summary = client.chat.completions.create(
model="grok-4.5",
messages=[{"role": "user", "content": f"Summary based on this analysis: {analysis}"}]
).choices[0].message.content
In this example, step 2 depends on the result from step 1. Async offers no benefit.
When Async Is Necessary
Async becomes essential as soon as network latency dominates your execution time and requests are independent. The textbook case is batch processing: dozens or hundreds of requests with no dependencies between them, where the gain is proportional to volume—one hundred two-second requests take three minutes in sequence, around ten seconds in controlled parallelism. The second case isn’t a choice but a requirement: in a FastAPI or Starlette application, the framework is already async, and using a sync client there would block the event loop—every call to Grok would freeze all other requests to your server. Then there are naturally concurrent architectures: a chatbot querying Grok, a database, and an external API in parallel to compose its response, or a monitoring system watching several streams at once—async is simply the natural fit for the problem there.
# Async — processing 50 questions in parallel
async def batch_process(questions: list[str]) -> list[str]:
sem = asyncio.Semaphore(5)
async def ask(q: str) -> str:
async with sem:
resp = await client.chat.completions.create(
model="grok-4.5",
messages=[{"role": "user", "content": q}]
)
return resp.choices[0].message.content
return await asyncio.gather(*[ask(q) for q in questions])
With 50 questions and a semaphore of 5, you process 5 requests in parallel instead of one. If each request takes 8 seconds, total time goes from 400 seconds to about 80 seconds.
The Premature Async Trap
Poorly designed async code is worse than well-written sync code. Here are common mistakes:
Blocking the Event Loop
# BAD: sync call in an async function
async def bad_example():
response = requests.get("https://api.example.com") # blocks everything
return response.json()
The requests library is sync. Calling it in a coroutine blocks the event loop and negates any benefit from async. Use httpx or aiohttp instead.
Unnecessary Complexity
# BAD: async for a single request
async def over_engineered():
response = await client.chat.completions.create(...)
return response
# Sync equivalent, simpler
def simple():
response = client.chat.completions.create(...)
return response
If you only have one request to send, async adds complexity without benefit.
Decision Tree
To choose between sync and async, ask yourself these questions in order:
- How many API requests are you sending? If one or a few in sequence, stay sync.
- Are the requests independent? If each request depends on the previous one, stay sync.
- Is your framework async? If yes (FastAPI, Starlette, aiohttp), use async to avoid blocking the event loop.
- Is total processing time an issue? If you’re processing batches and sync time is unacceptable, switch to async.
Key Takeaways
- Sync works for simple scripts, sequential pipelines, and prototyping
- Async is necessary for batch processing, async frameworks, and parallel requests
- Never use sync libraries (
requests) in async code - Complexity added by async must be justified by measurable gains
- Start sync, migrate to async when the need is confirmed