Asyncio RuntimeError: Event Loop Is Closed — How to Fix It
Close async client sessions (await session.close()) before the event loop shuts down or handle Windows ProactorEventLoop connection resets.
Root Cause Analysis
This error occurs when asynchronous code or a background destructor (__del__) attempts to schedule work, execute a callback, or close an async transport on an asyncio event loop that has already been terminated and closed.
1. Unclosed Async HTTP/Database Sessions at Shutdown
When using libraries like aiohttp or httpx, failing to explicitly call await session.close() before exiting asyncio.run() causes the Python garbage collector to invoke session.__del__() after the loop is already closed, raising RuntimeError: Event loop is closed.
2. Windows ProactorEventLoop Transport Teardown Bug
On Windows, Python defaults to ProactorEventLoop. When closing sockets during process termination, the underlying IOCP transport may attempt to write to closed loop handles.
3. Submitting Work to Closed Loops
Calling loop.run_until_complete() after manually invoking loop.close().
4. Pytest-Asyncio Fixture Scope Mismatches
Using function-scoped event loops with session-scoped database connection pools.
Reproduction Code (MCVE)
import asyncio
loop = asyncio.new_event_loop()
loop.close()
# Attempting to execute on a closed loop raises RuntimeError
loop.run_until_complete(asyncio.sleep(0.01))
Solution 1: Use Async Context Managers for Clean Resource Teardown
Always manage async sessions and connections with async with to ensure they close while the event loop is active.
import asyncio
class MockAsyncResource:
async def __aenter__(self):
print('Resource connected.')
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print('Resource safely closed before loop teardown.')
async def main():
async with MockAsyncResource():
await asyncio.sleep(0.01)
asyncio.run(main())
Solution 2: Windows ProactorEventLoop Workaround
On Windows Python 3.10+, configure event loop shutdown behavior or use SelectorEventLoop for specific test runners.
import asyncio
import sys
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
print('Configured WindowsSelectorEventLoopPolicy for clean teardown.')
A common mistake is creating global aiohttp.ClientSession() instances outside async def functions. Global sessions attach to whatever loop is active at import time and will fail on subsequent asyncio.run() calls. Always instantiate sessions inside async contexts. Edge cases occur in Jupyter Notebooks where the event loop is always running: use await coroutine() directly without asyncio.run(). Contrast this error with RuntimeError: This event loop is already running, which occurs when calling asyncio.run() inside an active loop.