A synchronous call made directly inside an asyncio coroutine runs on the event-loop thread. While that call is waiting or computing, the loop cannot run other tasks or handle their I/O. Prefer an async-native API; when a synchronous I/O call must remain, move it off the loop with asyncio.to_thread(). For CPU-heavy Python work, use an appropriate executor or process/interpreter boundary instead of expecting a thread to make it parallel.
Why synchronous code blocks the event loop
Asyncio uses cooperative scheduling: a task gives other work a chance to run when it awaits an operation that yields control. A regular synchronous function does not yield merely because it is called from an async def. If it performs a blocking operation or lengthy computation, it occupies the event-loop thread until it returns. Python’s Developing with asyncio guide warns that blocking CPU-bound code should not be called directly; even one second of such work delays every task and I/O operation sharing that loop for that second.
This is why making a wrapper async def does not make its internals non-blocking. The operation itself must use an async API that yields, or run outside the event-loop thread.
Calls that commonly cause trouble
time.sleep()in a coroutine, rather than an asynchronous wait.- Synchronous HTTP clients such as
requests, synchronous database drivers, or libraries that wait on network responses. - Blocking file operations and third-party calls whose I/O behavior has not been verified.
- CPU-heavy Python calculations, which can monopolize the loop even when they perform no I/O.
- Network logging handlers: the asyncio developer guide cautions that logging I/O can block, and recommends moving it to another thread or using non-blocking logging I/O.
Choose the right way to handle the work
Use an async-native client or API first when one is available: its I/O can yield to the event loop without occupying a worker thread. If the dependency is synchronous, choose the alternative according to whether it waits on I/O or consumes substantial CPU.
#1 Best Overall
| Approach | Best fit | Event-loop impact and concurrency | Context and cancellation | Control and compatibility |
|---|---|---|---|---|
| Async-native API | Network, database, or other I/O with a compatible async library. | Its awaited operations can yield so the loop can run other tasks. Actual throughput depends on the library and service. | Behavior depends on the API. Check its context and cancellation documentation. | Requires an async-compatible dependency and calling pattern. |
asyncio.to_thread() |
Small or moderate blocking I/O calls that must use a synchronous function. | Runs the function in a separate thread rather than on the loop. It is primarily intended for I/O-bound work; it does not by itself guarantee unlimited concurrency or greater CPU throughput. | Propagates the current contextvars.Context. Cancelling the task that awaits it does not automatically stop arbitrary synchronous work already running in the thread. |
Available from Python 3.9. Uses the loop’s default thread pool. |
loop.run_in_executor() with a thread pool |
Blocking calls when you need to select or configure an executor. | Moves the call off the event-loop thread. The executor’s capacity limits how many calls it can run at once. | Check context propagation and cancellation behavior for your executor and workload; cancellation of the awaiter is not a general way to stop running synchronous code. | Pass an executor explicitly, or pass None to use the loop’s default executor. Python documents that the default is lazily initialized as a ThreadPoolExecutor. |
| Interpreter or process executor | CPU-heavy work that should not run on the event-loop thread, especially when the usual single-interpreter GIL limits Python threads. | Moves computation across an interpreter or process boundary; process or interpreter execution can avoid the usual single-interpreter GIL bottleneck. Costs and throughput depend on the workload. | Account for the executor’s own result, error, and cancellation behavior; do not assume cancelling an await stops work already underway. | Requires code and data suitable for the selected boundary. Choose based on workload and isolation needs. |
| Fully synchronous architecture | An application whose surrounding design and dependencies are synchronous and do not need an asyncio event loop. | No asyncio loop is being blocked, but this approach does not provide asyncio’s cooperative task scheduling. | Uses the synchronous libraries’ behavior rather than asyncio task semantics. | Can avoid bridging sync and async dependencies, but is not a drop-in fix when the application already relies on asyncio. |
Python documents that asyncio.to_thread() was added in Python 3.9 and is mainly intended for I/O-bound functions that would otherwise block the loop. The GIL generally limits its usefulness for CPU-bound Python code; extension modules that release the GIL and Python implementations without that limitation can behave differently.
Move a blocking I/O call off the loop
For a synchronous function that waits on a file, database, network, or library operation, the concise option is:
Rank #2
result = await asyncio.to_thread(blocking_io, arg)
Pass the function itself, followed by its arguments. The coroutine awaits the result while the synchronous call runs in a separate thread. This does not convert the dependency into an async client; it keeps that blocking call from occupying the event-loop thread.
If you need explicit executor selection or management, use run_in_executor():
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →loop = asyncio.get_running_loop()
result = await loop.run_in_executor(executor, blocking_io, arg)
Here, executor is the executor you have chosen. Pass None instead to use the loop’s default executor. Python documents loop.set_default_executor(...) as the way to configure that default when its capacity or ownership should be explicit. Unlike to_thread(), this API takes positional arguments after the function; for keyword arguments, wrap the call in a suitable callable.
Handle CPU-heavy work differently
Moving CPU-heavy Python code to a thread keeps the event-loop thread free, but under the usual GIL it generally does not provide parallel execution of Python bytecode. For substantial CPU work, consider a process or interpreter executor when its isolation and data-transfer trade-offs suit the task. The asyncio developer guide recommends using an executor rather than calling blocking CPU-bound work directly.
Keep the boundary deliberate: send the calculation and the inputs it needs to the executor, then await its result. The right choice depends on the workload and how much isolation it needs; do not choose a process or interpreter pool solely because a thread-based call is easy to write.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Prevent common async/sync failures
Do not call synchronous work directly inside a coroutine
Replace a blocking dependency with an async-native equivalent if practical. Otherwise, wrap the synchronous I/O call in to_thread() or use an appropriate executor. Treat logging, file access, database calls, and third-party clients as potential blocking points until their behavior is known.
Best Value
Do not start a second event loop from async code
asyncio.run() is for starting a coroutine when no event loop is already running in that thread. Calling it from code that is itself running inside an event loop creates an integration error. In an async caller, use await to call the coroutine; keep loop startup at the application boundary.
Bound work submitted to threads
Submitting an unbounded volume of synchronous calls can overwhelm worker capacity or the dependency being called. Apply a concurrency limit, bounded executor, queue, or service-level limit appropriate to the library and service. There is no single safe pool size for every application: base the limit on the dependency’s capacity and the work’s behavior.
Design for cancellation not to stop the underlying call
If a task awaiting a worker-thread call is cancelled, do not assume the synchronous function has stopped. Use timeouts supported by the underlying library where available, and make operations safe to retry or complete idempotently where possible. Verify the behavior of the specific library and Python version rather than treating task cancellation as a way to interrupt arbitrary synchronous code.
Diagnose event-loop stalls
When unrelated tasks become slow together, look for synchronous calls on their shared loop, including calls hidden inside helper functions and logging. Enable asyncio’s development diagnostics while investigating latency or never-awaited coroutine bugs; Python’s Developing with asyncio guide describes these diagnostics and the risks of blocking logging I/O. Check suspected third-party calls as well as your own code: a function’s name or an async def wrapper does not establish that its internal work yields.
Recommended Free Tools
Quick Recap
A practical decision sequence
- Identify the blocking operation. Determine which call waits on I/O or performs lengthy computation, rather than assuming the whole coroutine is responsible.
- Look for an async-native equivalent. Use it when it supports the dependency and application pattern.
- For synchronous I/O, use
asyncio.to_thread(). Chooserun_in_executor()when explicit executor control is required. - For CPU-heavy Python, select a suitable executor boundary. Compare thread, interpreter, and process execution against the workload and isolation needs.
- Set a concurrency limit and check cancellation behavior. Make sure the dependency can handle the load and that the application does not mistake cancellation of an await for interruption of the synchronous operation.
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.

