Sync/Async Bridging
Real codebases mix blocking libraries with async frameworks. Bridge with asyncio.to_thread (3.9+), loop.run_in_executor, and asyncio.run only at entrypoints - never call asyncio.run from inside a running loop.
Search across all documentation pages
Real codebases mix blocking libraries with async frameworks. Bridge with asyncio.to_thread (3.9+), loop.run_in_executor, and asyncio.run only at entrypoints - never call asyncio.run from inside a running loop.
import asyncio
def blocking_io() -> str:
return "done"
async def main() -> None:
result = await asyncio.to_thread(blocking_io)
print(result)When to reach for this:
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
def slow_sync(n: int) -> int:
time.sleep(0.05)
return n * 2
async def via_to_thread() -> int:
return await asyncio.to_thread(slow_sync, 21)
async def via_executor() -> int:
loop = asyncio.get_running_loop()
with ThreadPoolExecutor(max_workers=2) as pool:
return await loop.run_in_executor(pool, slow_sync, 10)
async def main() -> None:
print(await via_to_thread())
print(await via_executor())
asyncio.run(main())What this demonstrates:
to_thread schedules callable on default thread poolrun_in_executor accepts custom executor for isolation| From -> To | Tool |
|---|---|
| async -> sync block | to_thread / executor |
| sync -> async entry | asyncio.run(main()) once |
| sync -> async mid-flight | asyncio.run_coroutine_threadsafe (advanced) |
ProcessPoolExecutor for CPU-heavy Pythonto_thread does not bypass GIL for compute| Alternative | Use When | Don't Use When |
|---|---|---|
| Native async client | Available for dependency | N/A |
| Sync framework (WSGI) | Entire app blocking | Need high concurrent I/O |
| anyio | Portable async abstraction | stdlib-only policy |
to_thread uses default executor - simpler; custom executor when isolation needed.
Default ~ min(32, cpu+4) - lower if tasks block long to avoid pile-up.
asyncio.run(expr()) in fresh shell only - not from inside async Jupyter without nest_asyncio (avoid in prod).
async def for await; sync def runs in thread pool automatically - still prefer true async I/O.
await loop.run_in_executor(process_pool, fn) - watch pickling and main guard.
contextvars.copy_context().run to propagate request id into to_thread work.
ORM still largely sync - sync_to_async wrapper with thread sensitivity flags.
If >30% route time in executor - migrate to async library or sync worker deployment model.
Yes in to_thread worker; never in coroutine body.
Mock slow_sync to increment counter; assert loop responsive via concurrent heartbeat task.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 19, 2026