[{"data":1,"prerenderedAt":597},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdebugging-event-loop-is-closed-runtimeerror\u002F":3},{"id":4,"title":5,"body":6,"description":566,"extension":567,"meta":568,"navigation":235,"path":593,"seo":594,"stem":595,"__hash__":596},"content\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdebugging-event-loop-is-closed-runtimeerror\u002Findex.md","Debugging \"Event loop is closed\" RuntimeError",{"type":7,"value":8,"toc":559},"minimark",[9,32,37,77,81,84,211,214,308,317,344,388,392,425,429,515,519,527,542,548,555],[10,11,12,16,17,20,21,24,25,27,28,31],"p",{},[13,14,15],"code",{},"RuntimeError: Event loop is closed"," almost always fires at the very end of a program or test, with a traceback pointing at a transport's ",[13,18,19],{},"__del__"," or a connection cleanup rather than your code. It means something scheduled work on an event loop that has already been closed. The three recurring causes are reusing a loop after ",[13,22,23],{},"asyncio.run"," closed it, calling ",[13,26,23],{}," more than once, and leaving tasks or transports dangling at teardown. This guide fixes each, including the ",[13,29,30],{},"pytest-asyncio"," variant.",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,58],"ul",{},[41,42,43,44,47,48,50,51,50,54,57],"li",{},"Python ",[13,45,46],{},"3.8+"," (",[13,49,23],{},", ",[13,52,53],{},"asyncio.all_tasks",[13,55,56],{},"asyncio.get_running_loop",").",[41,59,60,61,64,65,68,69,72,73,76],{},"For the test case: ",[13,62,63],{},"pytest-asyncio >= 0.23"," (the ",[13,66,67],{},"loop_scope"," parameter and ",[13,70,71],{},"asyncio_default_fixture_loop_scope"," setting were added in 0.23; earlier versions used the ",[13,74,75],{},"event_loop"," fixture).",[33,78,80],{"id":79},"solution","Solution",[10,82,83],{},"The state machine below shows where the error is raised — work entering a loop that has already transitioned to closed.",[85,86,89,207],"figure",{"className":87},[88],"diagram",[90,91,98,99,98,103,98,107,98,117,98,128,98,134,98,139,98,142,98,145,98,148,98,153,98,157,98,160,98,166,98,170,98,174,98,177,98,185,98,188,98,193,98,200,98,203],"svg",{"viewBox":92,"role":93,"ariaLabelledBy":94,"xmlns":97},"0 0 760 300","img",[95,96],"elc-t","elc-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[100,101,102],"title",{"id":95},"Event loop state and the error",[104,105,106],"desc",{"id":96},"A loop moves from created to running to closed; scheduling work after close raises RuntimeError: Event loop is closed.",[108,109,116],"text",{"x":110,"y":111,"textAnchor":112,"fontSize":113,"fontWeight":114,"fill":115},"380","30","middle","17","700","#3d405b","When the error is raised",[118,119],"rect",{"x":120,"y":121,"width":122,"height":123,"rx":124,"fill":125,"stroke":126,"strokeWidth":127},"40","70","170","58","12","#fffdf8","#81b29a","2",[108,129,133],{"x":130,"y":131,"textAnchor":112,"fontSize":132,"fontWeight":114,"fill":115},"125","96","13","created",[108,135,138],{"x":130,"y":136,"textAnchor":112,"fontSize":137,"fill":115},"116","11","asyncio.run starts",[118,140],{"x":141,"y":121,"width":122,"height":123,"rx":124,"fill":125,"stroke":126,"strokeWidth":127},"295",[108,143,144],{"x":110,"y":131,"textAnchor":112,"fontSize":132,"fontWeight":114,"fill":115},"running",[108,146,147],{"x":110,"y":136,"textAnchor":112,"fontSize":137,"fill":115},"tasks scheduled OK",[118,149],{"x":150,"y":121,"width":122,"height":123,"rx":124,"fill":151,"stroke":152,"strokeWidth":127},"550","#f4f1de","#e07a5f",[108,154,156],{"x":155,"y":131,"textAnchor":112,"fontSize":132,"fontWeight":114,"fill":115},"635","closed",[108,158,159],{"x":155,"y":136,"textAnchor":112,"fontSize":137,"fill":115},"loop torn down",[161,162],"line",{"x1":163,"y1":164,"x2":165,"y2":164,"stroke":115,"strokeWidth":127},"210","99","293",[167,168],"polygon",{"points":169,"fill":115},"293,99 283,94 283,104",[161,171],{"x1":172,"y1":164,"x2":173,"y2":164,"stroke":115,"strokeWidth":127},"465","548",[167,175],{"points":176,"fill":115},"548,99 538,94 538,104",[178,179],"path",{"d":180,"fill":181,"stroke":152,"strokeWidth":127,"strokeDashArray":182},"M635 128 C635 180 380 180 380 128","none",[183,184],"5","4",[167,186],{"points":187,"fill":152},"380,128 375,138 385,138",[108,189,192],{"x":190,"y":191,"textAnchor":112,"fontSize":124,"fill":152},"500","200","schedule after close",[118,194],{"x":195,"y":196,"width":197,"height":198,"rx":124,"fill":151,"stroke":115,"strokeWidth":199},"120","226","520","56","1.5",[108,201,15],{"x":110,"y":202,"textAnchor":112,"fontSize":132,"fontWeight":114,"fill":115},"251",[108,204,206],{"x":110,"y":205,"textAnchor":112,"fontSize":137,"fill":115},"271","dangling task, transport, or second run",[208,209,210],"figcaption",{},"The loop created by asyncio.run is closed when it returns; any task, transport, or second asyncio.run that touches it afterwards is scheduling work against a closed loop, which raises the error.",[10,212,213],{},"Run a single top-level coroutine and drain everything before the loop closes:",[215,216,221],"pre",{"className":217,"code":218,"language":219,"meta":220,"style":220},"language-python shiki shiki-themes github-light github-dark","import asyncio\n\nasync def main() -> None:\n    task = asyncio.create_task(worker())\n    try:\n        await do_work()\n    finally:\n        # Cancel and await stragglers so nothing is pending when run() closes\n        # the loop. return_exceptions=True swallows the CancelledError each raises.\n        task.cancel()\n        await asyncio.gather(task, return_exceptions=True)\n\n# ONE asyncio.run for the whole program. It creates a loop, runs main, closes it.\nasyncio.run(main())\n","python","",[13,222,223,230,237,243,249,255,261,267,273,279,285,291,296,302],{"__ignoreMap":220},[224,225,227],"span",{"class":161,"line":226},1,[224,228,229],{},"import asyncio\n",[224,231,233],{"class":161,"line":232},2,[224,234,236],{"emptyLinePlaceholder":235},true,"\n",[224,238,240],{"class":161,"line":239},3,[224,241,242],{},"async def main() -> None:\n",[224,244,246],{"class":161,"line":245},4,[224,247,248],{},"    task = asyncio.create_task(worker())\n",[224,250,252],{"class":161,"line":251},5,[224,253,254],{},"    try:\n",[224,256,258],{"class":161,"line":257},6,[224,259,260],{},"        await do_work()\n",[224,262,264],{"class":161,"line":263},7,[224,265,266],{},"    finally:\n",[224,268,270],{"class":161,"line":269},8,[224,271,272],{},"        # Cancel and await stragglers so nothing is pending when run() closes\n",[224,274,276],{"class":161,"line":275},9,[224,277,278],{},"        # the loop. return_exceptions=True swallows the CancelledError each raises.\n",[224,280,282],{"class":161,"line":281},10,[224,283,284],{},"        task.cancel()\n",[224,286,288],{"class":161,"line":287},11,[224,289,290],{},"        await asyncio.gather(task, return_exceptions=True)\n",[224,292,294],{"class":161,"line":293},12,[224,295,236],{"emptyLinePlaceholder":235},[224,297,299],{"class":161,"line":298},13,[224,300,301],{},"# ONE asyncio.run for the whole program. It creates a loop, runs main, closes it.\n",[224,303,305],{"class":161,"line":304},14,[224,306,307],{},"asyncio.run(main())\n",[10,309,310,311,313,314,316],{},"For ",[13,312,30],{},", match ",[13,315,67],{}," to the fixture scope so teardown runs on a live loop:",[215,318,322],{"className":319,"code":320,"language":321,"meta":220,"style":220},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"function\"   # 0.23+: stop recreating the loop per test\n","toml",[13,323,324,329,334,339],{"__ignoreMap":220},[224,325,326],{"class":161,"line":226},[224,327,328],{},"# pyproject.toml\n",[224,330,331],{"class":161,"line":232},[224,332,333],{},"[tool.pytest.ini_options]\n",[224,335,336],{"class":161,"line":239},[224,337,338],{},"asyncio_mode = \"auto\"\n",[224,340,341],{"class":161,"line":245},[224,342,343],{},"asyncio_default_fixture_loop_scope = \"function\"   # 0.23+: stop recreating the loop per test\n",[215,345,347],{"className":217,"code":346,"language":219,"meta":220,"style":220},"import pytest_asyncio\n\n# scope and loop_scope agree -> setup and teardown share one loop.\n@pytest_asyncio.fixture(scope=\"session\", loop_scope=\"session\")\nasync def client():\n    c = await open_client()\n    yield c\n    await c.aclose()        # runs on the SAME loop, not a closed one\n",[13,348,349,354,358,363,368,373,378,383],{"__ignoreMap":220},[224,350,351],{"class":161,"line":226},[224,352,353],{},"import pytest_asyncio\n",[224,355,356],{"class":161,"line":232},[224,357,236],{"emptyLinePlaceholder":235},[224,359,360],{"class":161,"line":239},[224,361,362],{},"# scope and loop_scope agree -> setup and teardown share one loop.\n",[224,364,365],{"class":161,"line":245},[224,366,367],{},"@pytest_asyncio.fixture(scope=\"session\", loop_scope=\"session\")\n",[224,369,370],{"class":161,"line":251},[224,371,372],{},"async def client():\n",[224,374,375],{"class":161,"line":257},[224,376,377],{},"    c = await open_client()\n",[224,379,380],{"class":161,"line":263},[224,381,382],{},"    yield c\n",[224,384,385],{"class":161,"line":269},[224,386,387],{},"    await c.aclose()        # runs on the SAME loop, not a closed one\n",[33,389,391],{"id":390},"why-this-works","Why this works",[10,393,394,396,397,401,402,405,406,408,409,412,413,415,416,418,419,424],{},[13,395,23],{}," is documented to create a fresh event loop, run the coroutine to completion, and then ",[398,399,400],"em",{},"close that loop"," before returning. Anything still bound to it — a database connection's transport, a background ",[13,403,404],{},"Task",", a ",[13,407,19],{}," cleanup — fires its callback against a loop that no longer accepts work, raising the error. Draining tasks and closing transports inside ",[13,410,411],{},"main"," (or a fixture's teardown) guarantees nothing is left to schedule after close. In ",[13,414,30],{},", the loop is owned by the ",[13,417,67],{},"; if a session fixture's teardown runs after a function-scoped loop has already closed, you hit the same wall, which is why aligning the scopes is the fix. The scope-versus-loop relationship is dissected in ",[420,421,423],"a",{"href":422},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fhow-to-scope-pytest-fixtures-for-async-tests\u002F","how to scope pytest fixtures for async tests",".",[33,426,428],{"id":427},"edge-cases-and-failure-modes","Edge cases and failure modes",[38,430,431,452,477,492,501],{},[41,432,433,440,441,443,444,447,448,451],{},[434,435,436,437,439],"strong",{},"Calling ",[13,438,23],{}," twice."," Each call closes its loop, so any object created in the first run is bound to a dead loop in the second. Use one ",[13,442,23],{}," and structure work as nested coroutines, or ",[13,445,446],{},"asyncio.Runner"," (3.11+) to reuse one loop across several ",[13,449,450],{},"run"," calls.",[41,453,454,466,467,469,470,459,473,476],{},[434,455,456,459,460,463,464,424],{},[13,457,458],{},"aiohttp"," \u002F ",[13,461,462],{},"asyncpg"," cleanup on ",[13,465,19],{}," Connections that schedule cleanup in ",[13,468,19],{}," raise this at interpreter shutdown. Always ",[13,471,472],{},"await session.close()",[13,474,475],{},"await conn.close()"," explicitly inside the loop.",[41,478,479,488,489,491],{},[434,480,481,484,485,424],{},[13,482,483],{},"loop.run_until_complete"," after ",[13,486,487],{},"loop.close"," Reusing a manually managed loop after closing it is the non-",[13,490,23],{}," form of the same bug. Do not close a loop you intend to reuse.",[41,493,494,500],{},[434,495,496,499],{},[13,497,498],{},"ProactorEventLoop"," on Windows."," Older Pythons raised this spuriously at shutdown on Windows even with correct code; upgrade to a current 3.x where it is fixed.",[41,502,503,506,507,509,510,514],{},[434,504,505],{},"Cross-loop objects in pytest."," An object built in a session fixture but used by a function-scoped loop straddles two loops. Match ",[13,508,67],{},", and see the ",[420,511,513],{"href":512},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fpytest-asyncio-vs-anyio-scoping-trade-offs\u002F","pytest-asyncio vs anyio scoping trade-offs"," for choosing a model.",[33,516,518],{"id":517},"frequently-asked-questions","Frequently Asked Questions",[10,520,521,524,526],{},[434,522,523],{},"Why does asyncio.run raise RuntimeError: Event loop is closed?",[13,525,23],{}," creates a new loop, runs the coroutine, then closes that loop. Calling it twice and reusing anything bound to the first loop, or leaving tasks and transports alive when it closes, raises the error because work is scheduled on a loop that no longer exists.",[10,528,529,532,533,535,536,538,539,541],{},[434,530,531],{},"How do I avoid the error with pytest-asyncio?","\nMatch the fixture's ",[13,534,67],{}," to its scope in ",[13,537,30],{}," 0.23 or newer so setup and teardown share one loop, and set ",[13,540,71],{}," so the loop is not recreated per test.",[10,543,544,547],{},[434,545,546],{},"Why does it only appear at the end of the program?","\nIt usually comes from teardown: a transport, connection, or task is still pending when the loop closes, so its cleanup callback fires against a closed loop. Cancel and await all tasks and close transports before the loop shuts down.",[10,549,550,551],{},"← Back to ",[420,552,554],{"href":553},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002F","Debugging Async Code and Event Loops",[556,557,558],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":220,"searchDepth":232,"depth":232,"links":560},[561,562,563,564,565],{"id":35,"depth":232,"text":36},{"id":79,"depth":232,"text":80},{"id":390,"depth":232,"text":391},{"id":427,"depth":232,"text":428},{"id":517,"depth":232,"text":518},"Fix RuntimeError: Event loop is closed from reusing a closed loop, calling asyncio.run twice, or dangling tasks at teardown, including pytest-asyncio loop_scope.","md",{"slug":569,"type":570,"breadcrumb":571,"datePublished":572,"dateModified":572,"faq":573,"howto":580},"debugging-event-loop-is-closed-runtimeerror","long_tail","Event loop is closed","2026-06-18",[574,576,578],{"q":523,"a":575},"asyncio.run creates a new loop, runs the coroutine, then closes that loop. Calling it twice and reusing anything bound to the first loop, or leaving tasks and transports alive when it closes, raises the error because work is scheduled on a loop that no longer exists.",{"q":531,"a":577},"Match the fixture's loop_scope to its scope in pytest-asyncio 0.23 or newer so setup and teardown share one loop, and set asyncio_default_fixture_loop_scope so the loop is not recreated per test.",{"q":546,"a":579},"It usually comes from teardown: a transport, connection, or task is still pending when the loop closes, so its cleanup callback fires against a closed loop. Cancel and await all tasks and close transports before the loop shuts down.",{"name":581,"description":582,"steps":583},"How to fix RuntimeError: Event loop is closed","Eliminate the error by using one loop per program, draining tasks before close, and matching pytest-asyncio loop scopes.",[584,587,590],{"name":585,"text":586},"Use one asyncio.run per program","Run a single top-level coroutine with asyncio.run and do all work inside it, instead of calling asyncio.run more than once.",{"name":588,"text":589},"Drain tasks before the loop closes","Cancel pending tasks and await them with gather and return_exceptions=True, and close transports and connections, before the loop shuts down.",{"name":591,"text":592},"Match pytest-asyncio loop scope","Set loop_scope equal to the fixture scope and configure asyncio_default_fixture_loop_scope so the loop is not recreated per test.","\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdebugging-event-loop-is-closed-runtimeerror",{"title":5,"description":566},"systematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdebugging-event-loop-is-closed-runtimeerror\u002Findex","cSZ0WdqBB_GcOzuMCPfeTdCsNPU_thf-phcDlmg6tko",1781793487407]