FastAPI/async "RuntimeError: Event loop is closed" in tests in CI
asyncio raises "Event loop is closed" when an async client, database engine, or task is awaited or finalized after the loop that owned it has shut down. In FastAPI test suites this usually means session/engine scope and loop scope do not line up.
What this error means
Async FastAPI tests fail (often at teardown or in the second test) with "RuntimeError: Event loop is closed", sometimes only when more than one async test runs.
RuntimeError: Event loop is closed
... in _check_closed
... during AsyncEngine.dispose() at session teardownCommon causes
Async fixtures outlive their event loop
A session-scoped async client or engine binds to one loop, but each test gets a new loop, so later use hits a closed loop.
Resources closed after the loop shut down
An async engine or client is disposed in a teardown that runs after the loop has already closed.
How to fix it
Align fixture scope with the event loop
- Use an async test plugin (anyio or pytest-asyncio) and keep async fixtures function-scoped, or pin a session-scoped loop.
- Create and dispose async engines/clients within the same loop the tests use.
- Use httpx.AsyncClient with an ASGI transport inside an async test.
import pytest, httpx
from httpx import ASGITransport
from app.main import app
@pytest.mark.anyio
async def test_root():
transport = ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://t") as c:
r = await c.get("/")
assert r.status_code == 200Pin one loop for the session if fixtures are shared
When async resources must be session-scoped, provide a single session-scoped event loop so all tests share it.
[tool.pytest.ini_options]
asyncio_mode = "auto"How to prevent it
- Keep async client/engine scope consistent with the loop scope.
- Create and dispose async resources inside the test loop.
- Prefer function-scoped async fixtures unless you pin one shared loop.