TaskGroups & timeouts

Running things concurrently: gather vs TaskGroup

Once a route needs to await two independent things โ€” say, a walker's profile and their bookings โ€” awaiting them one after another wastes time neither depends on the other. asyncio.gather(a, b) runs both concurrently and waits for both. It's been around for years and still works, but it has a sharp edge: if one awaitable raises, gather (by default) lets the other one keep running in the background โ€” it doesn't cancel it for you, and the exception doesn't surface until every task is done.

Python 3.11 added asyncio.TaskGroup โ€” structured concurrency: a with-block where every task you launch inside it is guaranteed to be finished (or cancelled) before the block exits. If ANY task raises, the group cancels every other task in the group immediately, waits for them to unwind, then re-raises. That's the behavior you want for a route: don't leave a stray query running after you've already decided to return a 500.