Free tools Windows power users keep installed
One-click scans. No signup required.
asyncio helps Python run multiple I/O-bound operations concurrently on one thread: a coroutine runs until it reaches an await that suspends it, giving the event loop a chance to run other ready work. It does not automatically make CPU-heavy Python code parallel. The Python documentation describes it as “a library to write concurrent code using the async/await syntax.”
This guide uses APIs available in Python 3.11 and later, including TaskGroup. Check the documentation for your specific Python release and platform when relying on newer or platform-specific features.
When should you use asyncio?
Use asyncio when a program spends substantial time waiting for network operations, subprocesses, or other asynchronous I/O, and you want to make progress on other work during those waits. It is commonly suited to network clients and servers, concurrent HTTP requests, and applications coordinating many I/O operations.
A synchronous program is often simpler when it performs a small amount of I/O or when its libraries are synchronous. For CPU-bound work such as heavy numerical computation, asyncio alone is not a parallel-computing solution: synchronous computation holds the event-loop thread until it finishes. Consider a process pool, native code that releases the GIL, or another suitable approach for CPU parallelism.
Recommended Free Tools
#1 Best Overall
- Good fit: many operations that spend time waiting and have asynchronous library support.
- Not an automatic speedup: CPU-bound code or blocking libraries called directly from an async task.
- Important prerequisite: the libraries used along the async path must themselves avoid blocking the event-loop thread, or blocking work must be moved elsewhere.
How asyncio scheduling works
An async def function defines a coroutine function. Calling it creates a coroutine object; it does not run the function to completion. The coroutine must be awaited by another coroutine or scheduled as a task.
import asyncio
async def greet(name: str) -> str:
await asyncio.sleep(0.1)
return f"Hello, {name}!"
async def main() -> None:
message = await greet("Ada")
print(message)
if __name__ == "__main__":
asyncio.run(main())
Save this as a Python file and run it with Python 3.11 or later. asyncio.run(main()) creates and manages the event loop for this top-level program, runs the coroutine, and closes the loop when it is done. For ordinary scripts, this is the recommended entry point rather than manually constructing and managing an event loop.
Concurrency is cooperative. While a task is running ordinary Python code, another task on that event loop does not preempt it. When it awaits an operation that suspends, the loop can run another ready task. An await does not guarantee a switch on every occasion: if the awaited operation is already complete, execution may continue without yielding.
Waiting asynchronously versus blocking
In the example, asyncio.sleep() suspends the coroutine without blocking the event-loop thread. By contrast, calling time.sleep() inside a coroutine blocks that thread; other tasks on the same loop cannot run during the block. The same risk applies to synchronous network calls and long-running CPU work.
Rank #2
import asyncio
import time
async def bad_wait() -> None:
time.sleep(2) # Blocks the event-loop thread.
async def good_wait() -> None:
await asyncio.sleep(2) # Lets other tasks run.
Do not use asyncio.sleep(0) as a replacement for making blocking code asynchronous. It yields control, but it does not make the blocking operation itself nonblocking.
Run related coroutines concurrently
Use a task group when several operations belong to one unit of work and should finish before that unit returns. asyncio.TaskGroup, available in Python 3.11 and later, provides structured concurrency: the group owns its child tasks, waits for them on exit, and handles child failures as a group.
import asyncio
async def fetch_label(label: str, delay: float) -> str:
await asyncio.sleep(delay)
return label
async def main() -> None:
async with asyncio.TaskGroup() as group:
first = group.create_task(fetch_label("first", 1.0))
second = group.create_task(fetch_label("second", 0.5))
print(first.result(), second.result())
asyncio.run(main())
The tasks overlap while waiting, so total waiting time is approximately the longer delay rather than the sum of both delays. This is an illustration of scheduling, not a general performance benchmark. After the async with block exits successfully, both tasks are complete and their results are available.
TaskGroup failure and cancellation
If a child task raises an exception other than asyncio.CancelledError, the group cancels its remaining child tasks, waits for their cleanup, and reports failures using an exception group. Handle failures outside the group with except* ExceptionType when you need to process matching exceptions from that group.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemstry:
async with asyncio.TaskGroup() as group:
group.create_task(do_work())
group.create_task(do_other_work())
except* OSError as errors:
for error in errors.exceptions:
print("I/O failure:", error)
Task cancellation is cooperative too. A task generally receives cancellation at an await point. If a coroutine needs cleanup, use try/finally; do not swallow cancellation unintentionally.
async def use_resource() -> None:
resource = await acquire_resource()
try:
await perform_work(resource)
finally:
await resource.close()
Cleanup in a finally block should be designed to complete safely under cancellation. Avoid catching BaseException or suppressing CancelledError unless you have a deliberate cancellation policy; structured task management depends on cancellation being allowed to propagate.
When to create a task directly
asyncio.create_task(coro) schedules a coroutine to run concurrently and returns a task. Keep a reference to tasks you create, await them, and decide how their errors and cancellation are handled. Untracked background tasks can outlive the code that logically owns them, have exceptions that go unnoticed, or be cancelled when the program shuts down.
For a group of related tasks, prefer TaskGroup. Direct task creation remains useful when a task genuinely has a longer or separately managed lifetime, but the owner must still retain, await, cancel, and observe it deliberately.
Useful high-level asyncio APIs
Start with high-level APIs rather than manipulating event loops, futures, transports, or protocols. The latter are primarily useful when building frameworks or libraries that need lower-level control.
- Network I/O: stream APIs such as
asyncio.open_connection()andasyncio.start_server()support coroutine-based TCP clients and servers. For HTTP, use an async HTTP client library rather than expecting the standard library’s synchronous HTTP calls to become nonblocking. - Queues:
asyncio.Queuelets producer and consumer coroutines exchange work without busy-waiting. A bounded queue can apply backpressure when producers outpace consumers. - Synchronization:
asyncio.Lock,Event,Condition, andSemaphorecoordinate tasks on an event loop. These are not general-purpose thread synchronization primitives. - Subprocesses: asyncio subprocess APIs allow a program to wait for child processes asynchronously. They are distinct from moving arbitrary CPU work into a process pool.
- Timeouts: use timeout facilities such as
asyncio.timeout()(Python 3.11+) to bound a region of asynchronous work. A timeout cancels the work; code must still clean up correctly.
Timeout example
import asyncio
async def main() -> None:
try:
async with asyncio.timeout(3):
await perform_request()
except TimeoutError:
print("The operation exceeded three seconds")
asyncio.run(main())
A timeout is not a hard interruption of arbitrary synchronous code. If perform_request() blocks the event-loop thread, the loop cannot deliver cancellation promptly. Use an asynchronous operation or isolate blocking work.
Calling blocking work and crossing threads
When a blocking I/O function has no async equivalent, asyncio.to_thread() can run it in a worker thread so the event loop remains available. This is generally a way to avoid blocking during I/O, not a way to parallelize pure Python CPU-bound work.
import asyncio
async def main() -> None:
result = await asyncio.to_thread(blocking_io_function, "argument")
print(result)
asyncio.run(main())
Many asyncio objects are not thread-safe. If another operating-system thread needs to schedule work on a running loop, use the loop’s thread-safe scheduling APIs, such as loop.call_soon_threadsafe(callback, *args) for a callback or asyncio.run_coroutine_threadsafe(coro, loop) for a coroutine. Do not call ordinary loop scheduling methods from an unrelated thread.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallBest Value
Debug asyncio problems
Enable debug mode during development to help expose incorrect API use and slow callbacks. You can pass debug=True to asyncio.run(), or use the PYTHONASYNCIODEBUG environment variable. The event loop can report callbacks that take too long; such reports are a useful sign to inspect blocking calls or unexpectedly expensive work.
import asyncio
async def main() -> None:
await do_work()
asyncio.run(main(), debug=True)
Debug mode can add overhead and is primarily a diagnostic aid. Treat slow-callback warnings as evidence that code is occupying the loop, not as a measure of total application performance.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
RuntimeWarning: coroutine was never awaited |
A coroutine was created by calling an async def function but neither awaited nor scheduled. |
Use await function() from async code, or create and manage a task with asyncio.create_task() or a task group. |
asyncio.run() cannot be called from a running event loop |
You called the script-level runner inside code that already runs an event loop, common in notebooks and async frameworks. | In an async function, use await main(). Let the framework or notebook own its loop. |
| Other tasks appear frozen during a request or delay | A synchronous blocking function is running on the event-loop thread. | Replace it with an async API, use asyncio.to_thread() for suitable blocking I/O, or use an appropriate process-based approach for CPU work. |
| A task’s exception is not handled where expected | The task may have been started without an owner awaiting it; a task group may report an exception group instead of a single exception. | Await the task or use a task group, and handle grouped failures with except* where appropriate. |
| Cancellation skips cleanup or leaves work running | Cancellation was swallowed, cleanup was omitted, or task lifetime was not managed. | Use try/finally for cleanup, generally let CancelledError propagate, and keep tasks within a clear owner such as a task group. |
| “Non-thread-safe operation” or erratic behavior across threads | An asyncio object or loop API was used from a thread other than the loop’s thread. | Schedule callbacks with call_soon_threadsafe() or coroutines with run_coroutine_threadsafe(). |
Or skip the browser setup
If your async project needs website screenshots, ScreenshotNeo provides a screenshot API; it is a separate service, not an asyncio feature. One GET request returns an image or PDF. For example, this cURL request saves a WebP screenshot of Stripe:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for parameters and response details. Cookie banners are accepted and removed along with supported newsletter popups and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. An MCP server provides screenshot tools for AI agents. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots. Learn about ScreenshotNeo or sign up free.
Further reading
The official Python asyncio library reference describes the API families and their version-specific behavior. Consult the documentation for the Python release you deploy, particularly for newer task and timeout APIs; development-branch documentation can change before release.
Frequently Asked Questions
Can I use asyncio in a Jupyter notebook?
Yes. A notebook typically already has a running event loop, so await a coroutine directly in a cell instead of calling asyncio.run() there.
Does asyncio require multiple CPU cores?
No. Its concurrency model lets a single event-loop thread coordinate work that yields while waiting; that is different from executing CPU-bound Python code in parallel.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




