TRT
Sign in
easyCoredesign

Compare `asyncio.TaskGroup` with `asyncio.gather`, explain the 'disappearing task' bug with `asyncio.create_task()`, and show how to protect a critical.

Contrast unstructured `gather` vs structured `TaskGroup`, explain weak-ref task collection, and trace `CancelledError` propagation + `asyncio.timeout()`.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

Production asyncio relies on three concurrency invariants: (1) Keep strong references to background tasks—the event loop only holds weak references to tasks, so fire-and-forget create_task() must add the task to a background_tasks: set[Task] with task.add_done_callback(background_tasks.discard). (2) Structured Concurrency (async with asyncio.TaskGroup() as tg:) guarantees that no child task outlives the async with block: if any child task raises an exception, TaskGroup automatically cancels all sibling tasks and waits for their finally blocks to finish before raising an ExceptionGroup (unlike asyncio.gather, which leaves orphan sibling tasks running in the background when one task fails!). (3) Bounded Backpressure (asyncio.Semaphore(k) or asyncio.Queue(maxsize=M)) caps in-flight concurrency so 50,000 jobs run at most k = 50 at a time.

Exiting an async with asyncio.TaskGroup() block guarantees that every child task spawned inside the group has reached a terminal state (completed, failed, or cancelled and cleaned up) before execution continues past the block. When a child task in a TaskGroup raises an unhandled exception (or the parent task is cancelled), the group immediately calls .cancel() on all remaining active child tasks (injecting asyncio.CancelledError at their current await point) and awaits their completion before propagating.

Capping concurrency with asyncio.Semaphore(50) or Queue(maxsize=100) bounds peak memory and socket descriptors to $O(k)$ instead of $O(N)$ across millions of items. In production systems (FastAPI/Starlette lifespan task groups, httpx connection pool limits, anyio nurseries, and vLLM's async request engine all enforce structured task groups and bounded semaphores.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how structured concurrency (asyncio.TaskGroup), CancelledError, and bounded backpressure operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Unbounded gather(*[fetch() for _ in 50k]), TaskGroup + Semaphore(50)) with concrete numbers.
  • Identifies real-world failure modes (Fire-and-forget asyncio.create_task(coro()) without saving the returned Task reference) and explains how production systems mitigate them.

Common mistakes

  • Treating structured concurrency (asyncio.TaskGroup), CancelledError, and bounded backpressure as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Catching CancelledError (or BaseException) and not re-raising it).
  • Forgetting boundary conditions and error recovery (Performing an await db.rollback() inside a finally: block when the task is already being cancelled).

This answer walks

Practice follow-ups

  1. 01Why did Python 3.8 move `asyncio.CancelledError` from subclassing `Exception` to subclassing `BaseException`?
  2. 02When using `asyncio.Queue(maxsize=100)` between producer and consumer tasks, how does `await queue.put(item)` create automatic backpressure all the way back to the network socket?

Not started

Sign in to save your learning progress.

Sign in to save