Python Asyncio for Faster API Scripts
When Python asyncio speeds up scripts that wait on many network calls, how to limit concurrency and handle errors and timeouts.

Use Python asyncio to speed up scripts that wait on many API calls: cap concurrency with a semaphore, set timeouts, handle failures, respect rate limits.
Many automation scripts spend most of their time waiting. A script that enriches a thousand records by calling an API one at a time sends a request, waits for the response, then sends the next. The processor sits idle while the network does its work, and the script takes as long as the sum of all those waits. Python asyncio lets a single script keep many requests in flight at once, so the waits overlap and the total run time can drop sharply, without the complexity of threads or multiple processes. It also makes it easy to send far more requests than an API allows, so concurrency needs limits. This guide explains when asyncio helps, how to cap concurrency, and how to handle timeouts and errors.
When Python asyncio helps
The Python documentation says asyncio is often a perfect fit for IO-bound and high-level structured network code (Python: asyncio). In automation terms, that means jobs dominated by waiting on networks or disks:
- Calling an API for each record in a list
- Downloading many files or pages
- Checking the status of many URLs or services
- Querying several systems and combining the results
It helps little for CPU-heavy work, such as parsing large files, image processing or complex calculations, because the bottleneck there is the processor rather than waiting. For that, look at multiprocessing or at making the code itself more efficient.
How asyncio works in brief
With asyncio, functions declared with async def are coroutines. When a coroutine reaches an await on a network call, it pauses and lets other coroutines run until the response arrives. An event loop schedules all of this in a single thread. To benefit, the libraries you use for network calls must support asyncio; a normal blocking HTTP library will hold up the whole loop while it waits. Choose an async HTTP client or database driver for the calls you want to overlap.
Limit concurrency with a semaphore
Starting a coroutine for every record at once is easy, and that is the danger. A list of ten thousand records can become ten thousand simultaneous requests, which will breach most API rate limits, exhaust connection pools and may look like an attack to the receiving service. Cap concurrency deliberately.
The asyncio library includes synchronisation primitives such as Semaphore. The documentation notes that these primitives are designed to resemble those in the threading module but are not thread-safe, so they should be used within the event loop rather than for synchronising operating system threads (Python: asyncio synchronization primitives). A semaphore created with a limit of, say, ten lets at most ten coroutines enter a block at the same time; the rest wait their turn.
Set the limit from the API's documented rate limits and your own testing, not from a guess. Our guide to API rate limit handling explains how to read and respect those limits, including backing off when the service asks you to slow down.
Worked example: bounded concurrent calls
The simplified, untested sketch below runs a hypothetical fetch for each record with a cap on concurrency and a timeout per call. The fetch function simulates a network call so the structure is clear.
# Simplified, untested sketch
import asyncio
MAX_CONCURRENT = 10
TIMEOUT_SECONDS = 15
async def fetch_record(record_id):
await asyncio.sleep(0.1) # stand-in for an async HTTP call
return {"id": record_id, "status": "ok"}
async def fetch_with_limit(sem, record_id):
async with sem:
try:
async with asyncio.timeout(TIMEOUT_SECONDS):
return await fetch_record(record_id)
except TimeoutError:
return {"id": record_id, "status": "timeout"}
async def main(record_ids):
sem = asyncio.Semaphore(MAX_CONCURRENT)
tasks = [fetch_with_limit(sem, rid) for rid in record_ids]
return await asyncio.gather(*tasks)
if __name__ == "__main__":
results = asyncio.run(main(range(50)))
print(sum(r["status"] == "ok" for r in results), "succeeded")
In a real script, fetch_record would use an async HTTP client, and results would be written to a file or database rather than printed.
Set timeouts on every call
A single hung request can hold a semaphore slot forever and slowly stall the whole job. Give every network call a timeout. The asyncio documentation describes asyncio.timeout as one of the components that enable structured concurrency, alongside TaskGroup (Python: Coroutines and Tasks). Many async HTTP clients also accept their own timeout settings; use both where available, with the client timeout for connection and read limits and an overall timeout as a backstop.
Note how the sketch catches TimeoutError outside the timeout block. The documentation explains that the context manager converts the cancellation into a TimeoutError, which can only be caught outside the context manager. Placing the try statement inside the block would miss it.
Check your Python version
Some of these tools are fairly recent. The documentation marks TaskGroup as added in Python 3.11, and you should check the documentation for your version before relying on newer timeout helpers. If your servers run an older Python, plan an upgrade or use the older equivalents described in the documentation for that version, and pin the version in your deployment so the script does not change behaviour unexpectedly.
Handle errors without losing results
When many calls run together, some will fail. Decide in advance how the script should behave:
- Catch expected errors inside each coroutine and return a result that records the failure, as in the sketch above
- Retry transient errors, such as timeouts and rate-limit responses, a small number of times with increasing delays
- Do not retry errors that will not change, such as a record that does not exist
- Write successful results as you go, so a crash late in the run does not lose earlier work
The same documentation describes TaskGroup as a more modern alternative to creating tasks individually; if one task in a group raises an unhandled exception, the others are cancelled. That behaviour suits jobs where one failure should stop everything. For jobs where each record is independent, catching errors inside each coroutine keeps the rest running.
Measure before and after
Run the job with a small sample first. Record total run time, the number of errors and the response times reported by the API. Then increase concurrency step by step and watch for rising errors or slower responses, which usually mean you have reached the service's limits. The best setting is often well below the point where the script is as fast as it can possibly be.
Keep the code readable
Async code can become hard to follow. Keep coroutines short, name them after what they do and keep the concurrency logic in one place, such as a single helper that applies the semaphore and timeout. Our guide to Python automation scripts covers the wider habits, such as logging and safe reruns, that matter just as much in async scripts.
Frequently Asked Questions
What is Python asyncio good for?
Work that spends most of its time waiting on networks or disks, such as calling an API for many records or downloading many files. The documentation describes it as often a perfect fit for IO-bound and structured network code.
Will asyncio speed up CPU-heavy work?
No. It overlaps waiting time, so CPU-bound tasks gain little. Use multiprocessing or optimise the code instead.
How do I avoid overloading an API with asyncio?
Use a semaphore to cap how many requests run at once, set the cap from the API's limits and back off when it signals you to slow down.
Can I use my normal HTTP library with asyncio?
A blocking library will hold up the event loop while it waits. Use an async HTTP client for calls you want to run concurrently.
Make slow scripts faster, safely
We redesign Python automation to run efficiently within API limits, with timeouts, retries and monitoring. See our custom app development services, or talk to us about Python asyncio.
About the Author
Manav Sharma is the founder of Insyrge and a CRM solutions architect. He works across Zoho, Salesforce, HubSpot, Odoo and more, designing the CRM setups, integrations and automations that keep sales and operations teams working from the same data.
Need Help Implementing This in Your Business?
Our certified Zoho consultants and automation experts can help you design and deploy custom workflows tailored to your operations.
Book Free Consultation