Skip to content
Latchkey

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.

python
RuntimeError: Event loop is closed
  ... in _check_closed
  ... during AsyncEngine.dispose() at session teardown

Common 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

  1. Use an async test plugin (anyio or pytest-asyncio) and keep async fixtures function-scoped, or pin a session-scoped loop.
  2. Create and dispose async engines/clients within the same loop the tests use.
  3. Use httpx.AsyncClient with an ASGI transport inside an async test.
python
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 == 200

Pin 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.

pyproject.toml
[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.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →