Async Python Highlights Summary
Every highlight bullet from the 10 pages in this section, gathered on one page and grouped by the page it came from.
- asyncio runs one coroutine's code at a time on one thread - concurrency comes from switching at await points, not from parallel execution
- A coroutine is a paused function object; await is the exact point where it hands control back to the loop
- gather does not cancel sibling awaitables when one fails - TaskGroup does, which is why structured concurrency exists
- Blocking the loop with synchronous code stalls every other coroutine scheduled on it, not just the one running
- Calling async def returns a coroutine; use asyncio.run to manage the event loop
- await pauses execution cooperatively, never blocking other tasks in the loop
- gather schedules multiple awaitables concurrently, returning results in order
- create_task runs coroutines as concurrent tasks; save references to prevent GC
- Use asyncio.sleep in coroutines; time.sleep blocks the entire event loop
- wait_for raises TimeoutError on expiry; prefer asyncio.timeout in Python 3.11+
- gather() fails fast on first exception unless return_exceptions=True
- Tasks start running immediately, coroutines only when awaited
- Keep references to created tasks to prevent garbage collection
- Blocking code inside tasks stalls all tasks; use to_thread
- Unbounded task creation exhausts memory without rate limiting
- Fire-and-forget tasks need error handlers to avoid silent failures
- TaskGroup (3.11+) ties child tasks to parent scope with guaranteed exit
- ExceptionGroup with except* handles multiple concurrent task failures
- Tasks created outside TaskGroup lose structured cancellation guarantees
- Blocking calls inside TaskGroup delay cancellation delivery to siblings
- Detach long-running jobs with supervisor tasks, not request TaskGroups
- Use asyncio.timeout for structured scope cancellation, not wait_for
- Share one AsyncClient per app to reuse TCP connections and avoid socket exhaustion
- Create client at app startup, close on shutdown to properly flush connection pools
- A new client per request destroys connection pooling and exhausts sockets immediately
- No default timeout means coroutines hang forever; set timeout on client and override per call
- Sync httpx in async routes blocks the event loop; use AsyncClient only in coroutines
- Unbounded gather to slow APIs overloads both peer and self; use semaphore to limit concurrency
- async with requires aenter and aexit as awaitable protocol methods
- async for calls anext between awaits until StopAsyncIteration is raised
- Never use sync with on async managers, await teardown with async with
- Blocking in aexit delays cancellation, use non-blocking close patterns
- Closing async generators requires async with aclosing() to avoid resource leaks
- aexit can return True to suppress exceptions like sync context managers
- Calling asyncio.run inside a running loop raises RuntimeError; await the coroutine instead
- asyncio.to_thread offloads blocking code so the event loop serves other coroutines
- CPU-bound work needs ProcessPoolExecutor as to_thread cannot bypass the GIL
- Limit thread creation with semaphores to prevent to_thread exhausting the pool
- Replace blocking ORM calls with native async drivers to avoid thread pool bottlenecks
- Allocate one ORM session per thread or task, never share sessions across boundaries
- Use acquire/release pattern with finally block to return connections to the pool
- Prefer one session per request with commit on success and rollback on exception
- Sync sessions in async routes block the event loop, use AsyncSession with await instead
- App-scoped engine pools prevent connection storms from per-request pool creation
- Missing await on execute returns a silent coroutine bug, add pyright ASYNC rules
- Eager load and batch queries to avoid N+1 problems even with async parallelism
- Unawaited coroutine warnings signal missing await on async calls
- Blocking I/O on the event loop stalls all concurrent tasks
- Use PYTHONASYNCIODEBUG=1 to surface slow callbacks and violations
- Shutdown hangs mean tasks weren't cancelled or properly awaited
- Sync locks across await cause cancel deadlocks, use asyncio.Lock
- pytest without asyncio_mode=auto produces false green tests
- Use async for many concurrent I/O waits on one process, not CPU-only batch scripts
- One long-lived event loop per worker via lifespan hooks, not per request
- Never block the event loop thread with time.sleep, sync requests, or sync DB calls
- Prefer TaskGroup for request-scoped child tasks to prevent orphaned tasks after response
- Share AsyncClient and pools at app scope, created in lifespan and sized to DB limits
- Limit fan-out with semaphores and always log task exceptions to avoid silent failures
Reviewed by Chris St. John·Last updated Jul 31, 2026