With the default return_exceptions=False, asyncio.gather propagates the first exception to the caller immediately. It does not cancel the remaining awaitables: they keep running in the background, and if one of them later fails, the failure surfaces as an unretrieved task exception, or not at all.
This is the practical difference from TaskGroup, which cancels siblings and waits for them. Under gather, a failing request in a batch of ten leaves nine sockets open and nine tasks scheduled after the handler has already returned an error to the client.
When you need every result including failures, pass return_exceptions=True, which returns exception objects in place of results and keeps the ordering aligned with the inputs; remember to inspect them, since nothing raises. When you need fail-fast with cleanup, use TaskGroup on Python 3.11 and later. If gather must stay, keep the task objects and cancel them explicitly in a finally block, then await them so cancellation actually completes before you return.