Real programs mix in sync libraries; offloading keeps the event loop responsive.
When you must call blocking code from async code (a sync SDK, file I/O, time.sleep, a C library), don't call it directly: it stalls every coroutine. Instead:
result = await asyncio.to_thread(func, *args, **kwargs) runs func in the loop's default ThreadPoolExecutor and awaits the result. Context variables are propagated. Because blocking I/O releases the GIL, many such calls can overlap.
For CPU-bound Python code, threads don't help on a standard (GIL) build. Use a process pool:
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
r = await loop.run_in_executor(pool, cpu_heavy, data)run_in_executor(None, func, *args) uses the default thread pool and accepts only positional args (wrap with functools.partial for keywords).
The opposite direction, calling async code from another thread, uses asyncio.run_coroutine_threadsafe(coro, loop), which returns a concurrent.futures.Future, and loop.call_soon_threadsafe(cb) for plain callbacks. Never touch loop objects from other threads without these.
The default executor has a limited number of worker threads (min(32, os.process_cpu_count() + 4) in 3.13), so thousands of slow blocking calls will queue up; for that scale, find an async library.
Example
import asyncio, time
def blocking_io(n):
time.sleep(0.5) # a sync library call
return n * 10
async def heartbeat():
for _ in range(3):
print("tick")
await asyncio.sleep(0.2)
async def main():
t0 = time.perf_counter()
results = await asyncio.gather(
asyncio.to_thread(blocking_io, 1),
asyncio.to_thread(blocking_io, 2),
heartbeat(), # keeps ticking meanwhile
)
print(results[:2], f"{time.perf_counter() - t0:.1f}s")
asyncio.run(main())
# tick / tick / tick, then: [10, 20] 0.6sRemember
- asyncio.to_thread(func, ...) awaits a blocking call without freezing the loop
- Use run_in_executor with a ProcessPoolExecutor for CPU-bound work
- run_coroutine_threadsafe submits coroutines from other threads
Watch out for
- Using to_thread for CPU-heavy pure-Python work and expecting a speedup on a GIL build.