Bug report
Bug description:
On Python 3.14, asyncio.wait(..., return_when=FIRST_COMPLETED) permanently retains every
completed waiting task when one of the awaited futures is long-lived (never resolves). Each call
registers the waiting task on the long-lived future's await-graph (_asyncio_awaited_by) but never
discards it once the wait returns, so the task is pinned until that future finally resolves โ which,
for a "signal" future used as a cancellation/close marker, can mean the lifetime of the process.
This is the same omission already fixed for asyncio.staggered.staggered_race in
gh-129195 (add to awaited_by โ must discard), but asyncio.wait still has it.
import asyncio, gc async def main(): loop = asyncio.get_running_loop() immortal = loop.create_future() # never resolves (e.g. a connection-close signal) def done_tasks(): gc.collect() return sum(1 for o in gc.get_objects() if isinstance(o, asyncio.Task) and o.done()) async def race_once(): await asyncio.wait( {asyncio.ensure_future(asyncio.sleep(0)), immortal}, return_when=asyncio.FIRST_COMPLETED, ) base = done_tasks() for _ in range(1000): await asyncio.create_task(race_once()) print("retained done tasks:", done_tasks() - base) immortal.cancel() asyncio.run(main())
3.13: retained done tasks: 1
3.14: retained done tasks: 1000 # grows linearly with calls; freed only when `immortal` resolves
Related
- await-graph feature: Async Call-Stack Reconstruction #91048 / PR GH-91048: Add utils for capturing async call stack for asyncio programs and enable profiling #124640
- prior identical fix in stdlib: asyncio staggered race missing calls to asyncio.future_add_to_awaited_by() and asyncio.future_discard_from_awaited_by() #129195 (
staggered_racemissing discard) - surfaced as [Bug]: context.route retains completed Tasks until Connection close (memory leak) microsoft/playwright-python#3121
CPython versions tested on:
CPython main branch, 3.14
Operating systems tested on:
macOS
Linked PRs
- gh-152569: Fix asyncio.wait leaking tasks via await-graph on long-lived futures #152585
- [3.14] gh-152569: Fix asyncio.wait leaking tasks via await-graph on long-lived futures (GH-152585) #152866
- [3.13] gh-152569: Fix asyncio.wait leaking tasks via await-graph on long-lived futures (GH-152585) #152867
- [3.15] gh-152569: Fix asyncio.wait leaking tasks via await-graph on long-lived futures (GH-152585) #152868