On this page
The core idea
asyncio runs many coroutines on a single thread. When one coroutine awaits something slow, such as a network response or a database query, the event loop switches to another coroutine that is ready to run. This overlaps waiting time, which is why fetching a hundred URLs concurrently can take roughly as long as the slowest few rather than the sum of all of them. It does not run Python code in parallel. A coroutine that performs a long calculation without awaiting blocks every other coroutine until it finishes.
A practical approach
Step 1
Confirm the work is I/O-bound: most time is spent waiting on networks, disks or databases, not computing.
Step 2
Use async-capable libraries throughout, such as an async HTTP client and an async database driver. Calling a blocking library inside a coroutine blocks the whole loop.
Step 3
Start concurrent work with asyncio.gather or, on Python 3.11 and later, asyncio.TaskGroup, which also cancels sibling tasks if one fails.
Step 4
Limit concurrency with asyncio.Semaphore so you do not open hundreds of connections at once or exceed an API's rate limit.
Step 5
Move unavoidable blocking or CPU-heavy calls off the loop with asyncio.to_thread, or into a process pool for real CPU work.
Worked example
A script checks 500 course links by requesting each URL in sequence and takes about eight minutes. Rewriting it with an async HTTP client, a semaphore limiting concurrency to 20 and a TaskGroup brings the run down to well under a minute. A later version that also parses large PDFs inside the same coroutines slows down again, because parsing is CPU-bound; moving parsing to a process pool restores the speed.
A mistake to avoid
Do not call time.sleep() or a synchronous HTTP library inside async code. They block the event loop and silently remove the benefit of async. Use await asyncio.sleep() and async clients instead.
Try it yourself
Write a script that fetches ten pages sequentially, then rewrite it with asyncio and a semaphore of three. Time both versions, then add a deliberately CPU-heavy function inside one coroutine and observe its effect on the others.
Your implementation checklist
- I use asyncio only for I/O-bound work
- Every library called in coroutines is async-capable
- Concurrency is limited with a semaphore
- Task groups or gather manage concurrent tasks and failures
- Blocking and CPU-heavy work runs outside the event loop
Share or cite this guide
Innovate with Ajay Editorial (2026-10-07). Use Python asyncio for Waiting, Not for Heavy Computation. https://innovatewithajay.com/blogs/use-asyncio-for-waiting-not-computation
