Python asyncio
Asyncio wins for high-concurrency I/O waiting, not for CPU work and not for a handful of calls. Its whole model rests on one rule: never block the event loop, because one blocking call stalls every other task.
Method
- Confirm async is the right tool. Use it when you have many concurrent I/O-bound operations (hundreds of sockets, requests, or DB calls) that spend their time waiting. For CPU-bound work use processes; for a few sequential calls, plain synchronous code is simpler and faster to reason about. See python-concurrency for the decision table.
- Keep the loop pure I/O. Any synchronous call that blocks (a
requestscall,time.sleep, heavy computation, a blocking driver) freezes all tasks. Push it off the loop withasyncio.to_thread(fn, ...)for I/O or a process pool for CPU. EnablePYTHONASYNCIODEBUG=1or passdebug=Truetoasyncio.runto log slow callbacks. - Structure concurrency with TaskGroup. On 3.11+,
async with asyncio.TaskGroup() as tg: tg.create_task(...)awaits all children and cancels siblings if one fails, surfacing errors as an ExceptionGroup. It replaces baregather, which leaves orphaned tasks running when one task raises and you forgetreturn_exceptions. - Use gather only for its shape.
gather(*coros)fits a fixed fan-out where you want results positionally and are ready to handle partial failure explicitly. Do not fire tasks withcreate_taskand drop the reference; the loop may garbage-collect them mid-flight. Hold references or use a TaskGroup. - Treat cancellation as cooperative. Cancelling raises
CancelledErrorat the next await; always re-raise it rather than swallowing it in a broadexcept. Guard cleanup withfinally, and bound external calls withasyncio.timeout()so a stuck peer cannot hang a task forever. - Never nest or block the loop from sync code. One
asyncio.runper program entry; calling it from inside a running loop raises. To bridge sync callers, useasyncio.runat the top only, and neverloop.run_until_completeinside async code.
Boundaries
- Async does not speed up CPU-bound work; the GIL still serializes it. Reach for multiprocessing instead.
- One blocking library call defeats the model. If a dependency has no async client, wrap it in a thread or pick a different tool.
- Mixing async and threads shares no automatic safety; the loop is not
thread-safe, so hand work across with
run_coroutine_threadsafe.