Introduction

Python's asyncio module throws RuntimeError: This event loop is already running or RuntimeError: asyncio.run() cannot be called from a running event loop when code attempts to start or manipulate an event loop from within an already-running loop. This error is common in Jupyter notebooks, web frameworks with async support, and applications that mix synchronous and asynchronous code. The root cause is that Python's asyncio model uses a single event loop per thread, and calling asyncio.run() creates a new loop -- which conflicts with the existing one.

Symptoms

bash
RuntimeError: asyncio.run() cannot be called from a running event loop
  File "/usr/lib/python3.11/asyncio/runners.py", line 35, in run
    raise RuntimeError(

Or in Jupyter/IPython:

bash
RuntimeError: This event loop is already running
  File "/home/user/.local/lib/python3.11/site-packages/nest_asyncio.py", line 73

Or when closing a closed loop:

bash
RuntimeError: Event loop is closed
  File "uvloop/loop.pyx", line 1520, in uvloop.loop.Loop.run_until_complete

Common Causes

  • Calling asyncio.run() inside an async function: asyncio.run() is for top-level entry points only
  • Jupyter notebook already running a loop: IPython starts its own event loop
  • Nested asyncio.run() calls: Library code calls asyncio.run() from within async context
  • Loop closed before coroutines complete: Using loop.close() prematurely
  • Threading with event loops: Creating loop in one thread, running in another
  • uvloop conflicts: Third-party loop implementation not compatible with asyncio.run()

Step-by-Step Fix

Step 1: Use asyncio.run() only at top level

```python import asyncio

# WRONG: Calling asyncio.run() inside async context async def fetch_data(): # Some async work return {"data": "value"}

async def main(): # WRONG - will raise RuntimeError result = asyncio.run(fetch_data()) return result

# CORRECT: asyncio.run() only at module level async def main(): result = await fetch_data() return result

asyncio.run(main()) ```

Step 2: Use nest_asyncio for Jupyter compatibility

```python # Install: pip install nest_asyncio import nest_asyncio nest_asyncio.apply()

import asyncio

# Now this works in Jupyter notebooks async def fetch_data(): await asyncio.sleep(1) return "done"

# Can call this from Jupyter cells without RuntimeError result = await fetch_data() print(result) ```

Step 3: Check for running loop before creating one

```python import asyncio

def get_or_create_loop(): """Safely get existing loop or create new one.""" try: loop = asyncio.get_running_loop() # Already in async context - return the loop return loop, True except RuntimeError: # No running loop - need to create one loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) return loop, False

# Usage in mixed sync/async code def sync_wrapper(coro): loop, running = get_or_create_loop() if running: return asyncio.ensure_future(coro) else: return loop.run_until_complete(coro) ```

Prevention

  • Reserve asyncio.run() exclusively for the top-level entry point
  • Use asyncio.get_running_loop() to detect existing loop context
  • Use nest_asyncio in Jupyter notebooks or IPython environments
  • Never call loop.close() on the running loop -- let asyncio.run() handle cleanup
  • Use asyncio.create_task() instead of spawning new loops for concurrent work
  • Document async boundaries in your codebase to prevent accidental nesting
  • Test async code with both CPython's default loop and uvloop in CI