Python — RuntimeError: asyncio.run() cannot be called from a running event loop
Resolve asyncio.run() cannot be called from a running event loop in notebooks and frameworks, find the outer loop owner, and await coroutines directly instead.
Symptoms
- RuntimeError: asyncio.run() cannot be called from a running event loop when calling a library function from Jupyter or inside FastAPI handlers
- The same code runs fine as a plain python script but fails in a notebook or web framework
- The stack shows the failing asyncio.run nested inside a framework-managed handler
Possible causes
- Jupyter already runs an event loop in the notebook kernel, so asyncio.run tries to nest a second loop
- FastAPI or another async framework owns the loop that invokes the code, and a sync helper calls asyncio.run
- A library exposes a sync wrapper around async code and calls asyncio.run internally when already inside a loop
- Test runners with event loop fixtures re-enter async code through sync helpers
Diagnosis steps
Confirm a loop is already active
python -c "import asyncio; print(asyncio.get_running_loop())" # inside the failing contextInterpretation
A returned loop object proves an outer loop exists; the error is the guard against nesting, not a broken install.
Identify the loop owner
python -c "import asyncio; loop=asyncio.get_running_loop(); print(loop, asyncio.current_task())"Interpretation
The current task and its name usually reveal the framework integration that owns execution.
Locate nested run calls
grep -Rn "asyncio.run(\|get_event_loop().run_until_complete" src/ | headInterpretation
Call sites reachable from async contexts are the re-entry points to refactor.
Root cause
asyncio.run creates a new event loop, runs the coroutine to completion, and closes the loop. Calling it while another loop is already running would nest two loops in one thread, which asyncio forbids by raising the RuntimeError — the error is a correctness guard, not a malfunction.
Fix
Inside an async context, await the coroutine directly. For sync code that must call async code, offload to a worker thread with its own loop. In notebooks, either use top-level await or run blocking code in a thread executor. Prefer asyncio.run as a program entry point only.
Risk note
nest_asyncio patches the guard but is a monkeypatch that can interact badly with library internals and cleanup. Use it only as a last resort in notebooks.
Verify the fix
Invoke the previously failing code path from every context (notebook cell, framework handler, CLI) and assert completion without RuntimeError; confirm loop count stays one per thread.
Symptom walkthrough
The error text states the rule it enforces:
RuntimeError: asyncio.run() cannot be called from a
running event loop
The context asymmetry is the fingerprint: a plain python script works because no loop is running, while a notebook cell or a FastAPI handler fails because the framework already owns a loop in this thread. Verify the ownership from inside the failing context:
import asyncio
asyncio.get_running_loop()
# <_UnixSelectorEventLoop running=True closed=False debug=False>
running=True means the environment, not your code, is the host — and nesting a second loop is what the guard forbids.
Fix in practice
Match the call shape to the context you are in. Already async: await directly.
async def handler():
result = await fetch_all(urls) # no run() anywhere
Sync code that must drive async code: give it a private loop in a worker thread.
import asyncio, threading
def run_async(coro):
result = {}
def target():
result["value"] = asyncio.run(coro)
t = threading.Thread(target=target)
t.start(); t.join()
return result["value"]
Notebook cells: use top-level await (Jupyter supports it), or push blocking work to an executor thread. Reserve asyncio.run for genuine program entry points — the first sync frame of a CLI or script.