Async and concurrency
By the end: You can run many slow API calls at once without exceeding a rate limit.
- Why async exists and when it helps
- async, await and asyncio.run
- Running many things at once with gather
- Limiting how many run at once
- Timeouts, and the mistake that silently undoes all of it
New to python? A programming language is the set of written instructions a computer will follow, and Python is the one this field settled on. It reads close to English: a line that filters a list of records looks roughly like a sentence describing that filter. Every model provider ships a Python library, and nearly every machine learning tool is written in it. Why it matters
Calling a model API is mostly waiting. Make a hundred calls one after another and your program spends nearly its whole life doing nothing, so the fix is to have many in flight at once. This section covers how Python expresses that, how to start many and collect the results, how to cap the number running so you do not trip a rate limit, and the one mistake that quietly turns concurrent code back into sequential code.
The problem async solves#
A call to a model API takes a few seconds, and your program spends nearly all of that time doing nothing but waiting for the reply. If you make fifty calls one after another, you wait fifty times.
# 50 calls, 2 seconds each, about 100 seconds total
for doc in docs:
summarise(doc)Async lets you start all fifty and wait for them together. The total becomes roughly the time of one call. This is the single biggest speed improvement available in most code that talks to an API.
The syntax#
Mark a function async def, and use await when calling something that takes time. A function marked async does not run on its own, it has to be started by asyncio.run or awaited from another async function.
import asyncio
async def summarise(doc: str) -> str:
return await call_model(doc)
async def main():
result = await summarise("some text")
print(result)
asyncio.run(main()) # starts everythingRunning many at once#
gather starts everything together and waits for all of it. Results come back in the order you passed them in.
async def main(docs):
results = await asyncio.gather(*(summarise(d) for d in docs))
return resultsBy default, if one of them raises, gather cancels the rest and raises. In a batch job that is usually wrong: one bad document should not lose the other forty-nine. Pass return_exceptions=True and the failures come back as values you can inspect.
results = await asyncio.gather(*tasks, return_exceptions=True)
for doc, result in zip(docs, results):
if isinstance(result, Exception):
log(f"failed: {doc}", result)
else:
save(result)Not too many at once#
Firing five hundred requests simultaneously will get you rate limited. A semaphore is a counter that only lets a fixed number through at a time, and queues the rest.
async def run_all(docs, limit=8):
sem = asyncio.Semaphore(limit)
async def one(doc):
async with sem: # waits for a free slot
async with asyncio.timeout(60): # give up after 60s
return await summarise(doc)
return await asyncio.gather(*(one(d) for d in docs), return_exceptions=True)Put a timeout on anything that touches the network. Without one, a single stalled connection holds a slot forever and the job never finishes.
The mistake that undoes everything#
One blocking call freezes all of it
Async runs on a single thread. If you call something that blocks, such as an ordinary requests.get, time.sleep, or a heavy file read, everything else stops until it finishes. Your concurrent code then runs at exactly sequential speed, and people usually blame the API. Use an async library, or hand the blocking work to asyncio.to_thread.
import requests, httpx, asyncio
# Blocks the whole event loop
async def bad(url):
return requests.get(url).text
# Correct: an async client
async def good(url):
async with httpx.AsyncClient() as client:
r = await client.get(url)
return r.text
# When you have no async option
async def fallback(path):
return await asyncio.to_thread(heavy_sync_read, path)When async does not help#
Async only helps when you are waiting. If the work is calculation rather than waiting, there is still only one thread doing all of it, so async adds complexity and speeds up nothing. For heavy computation you want multiprocessing instead.
Where people go wrong
Forgetting await. Calling an async function without it returns a coroutine object rather than running anything, and Python only warns you that a coroutine was never awaited. Nothing happens and nothing obviously fails.
If this comes up in an interview
What is the difference between concurrency and parallelism?
Concurrency means several tasks are in progress and taking turns. Parallelism means they genuinely run at the same instant on different cores. Python's asyncio gives you concurrency on one thread, which is enough when the bottleneck is waiting for the network.
When does async not help?
When the work is CPU-bound. Async overlaps waiting, not computing, and there is still one thread. For heavy computation you need multiprocessing.
What happens if you call a blocking function inside a coroutine?
It stalls the entire event loop, so every other coroutine stops until it returns. The symptom is that concurrent code runs at sequential speed. The fix is an async library or asyncio.to_thread.
How would you call an API 1000 times without being rate limited?
Use asyncio.gather with a Semaphore capping how many are in flight, a timeout on each request, return_exceptions=True so one failure does not lose the rest, and retry with exponential backoff on 429 responses.
Build this: Fetch fifty URLs with at most five running at once, a per-request timeout, and a summary at the end of which succeeded and which failed. Then swap in a blocking call and watch the speed collapse back to sequential.
Watch it explained
This is one section of Python, 33 hours across 9 sections. Read it in full to get the whole topic in order.
Stop 1 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.