Skip to content

Modern Async Practices

Use async code for concurrent I/O under an async call path. Keep task ownership, cancellation, and resource limits explicit.

Structured Concurrency

Prefer asyncio.TaskGroup on Python 3.11+ when sibling tasks form one operation:

async with asyncio.TaskGroup() as task_group:
    user_task = task_group.create_task(fetch_user(user_id))
    orders_task = task_group.create_task(fetch_orders(user_id))

user = user_task.result()
orders = orders_task.result()

The context owns the tasks, waits for completion, and cancels siblings when one fails. Use asyncio.gather when its result ordering or independent-failure semantics are specifically required, not as the automatic fan-out primitive.

Cancellation and Timeouts

  • Treat cancellation as control flow. Cleanup may run, but normally re-raise asyncio.CancelledError after cleanup.
  • Prefer asyncio.timeout on Python 3.11+ for a bounded block containing one or more awaits.
  • Place timeouts at the layer that knows the operation's latency budget. Nested library timeouts should remain narrower implementation safeguards.
  • Use asyncio.shield sparingly for work that must outlive caller cancellation, and retain a strong reference to the task.

Blocking Work and Backpressure

  • Never call blocking network, file, subprocess, or lock APIs on the event-loop thread.
  • Use native async libraries for sustained I/O. Use asyncio.to_thread for bounded blocking I/O that has no async API; it does not make CPU-bound Python work parallel.
  • Bound task creation with worker pools, semaphores, or bounded queues. Unlimited fan-out turns downstream latency into memory and connection exhaustion.
  • Do not hold a threading.Lock across await. Use asyncio.Lock for event-loop coordination and minimize the protected region.

Task and Resource Ownership

  • Keep a strong reference to background tasks and define how their exceptions are observed.
  • Do not create fire-and-forget tasks for work whose completion affects correctness. Put durable work behind a supervised worker or queue.
  • Use async with for clients, streams, sessions, locks, and other asynchronous resources.
  • At synchronous entry points, prefer asyncio.run; use asyncio.Runner when several top-level async calls must share one loop and context.

Reference: asyncio documentation.