asyncio.run cannot be called from a running event loop when using Jupyter Notebook
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When you call asyncio.run() inside a Jupyter notebook, Python raises RuntimeError: This event loop is already running. The reason is straightforward: Jupyter's kernel already manages its own asyncio event loop for interactive execution, and asyncio.run() tries to create a second top-level loop, which asyncio does not allow.
The fix in most cases is to stop using asyncio.run() in notebooks and instead await your coroutines directly at the cell level. Jupyter supports top-level await natively, making it the cleanest solution.
Why the Error Happens
asyncio.run() is designed as the single entry point for a standalone async program. Under the hood, it does three things:
- Creates a new event loop
- Runs the given coroutine to completion on that loop
- Closes the loop when the coroutine finishes
In a Jupyter notebook, the IPython kernel starts an event loop during initialization and keeps it running for the entire session. When your cell calls asyncio.run(), it attempts step 1 (create a new loop) while the kernel's loop is already active. Python's asyncio explicitly forbids nested loops, so it raises the error.
The same issue can appear in other environments that run their own event loop, including certain GUI frameworks (Tkinter, Qt), web servers (FastAPI/Uvicorn during testing), and some IDE debuggers.
Solution 1: Top-Level await (Recommended)
Modern Jupyter notebooks (IPython 7.0+ with Python 3.8+) support top-level await directly in cells:
This works because Jupyter hooks into the already-running event loop and schedules the coroutine on it. There is no conflict because no second loop is created.
Multiple Coroutines in One Cell
You can gather multiple coroutines in a single cell:
Both coroutines run concurrently on Jupyter's existing loop.
Solution 2: asyncio.create_task() for Background Work
When you want work to run concurrently without blocking the cell, create a task:
This is useful for long-running operations where you want to check progress or run other cells while waiting.
Solution 3: nest_asyncio (Compatibility Fallback)
When you cannot modify the code that calls asyncio.run() (for example, a third-party library), nest_asyncio patches the event loop to allow re-entrant execution:
Install it with:
When nest_asyncio Is the Right Choice
| Scenario | Use nest_asyncio? |
| You control the notebook code | No, use top-level await |
A library internally calls asyncio.run() | Yes |
| You are porting a script to a notebook temporarily | Maybe, but refactoring to await is better |
You are running tests that use asyncio.run() in notebooks | Yes |
| Production code | No, fix the architecture instead |
nest_asyncio is a compatibility shim, not an architectural solution. Prefer restructuring your code when possible.
Writing Code That Works in Both Scripts and Notebooks
The cleanest approach is to keep async logic in coroutines and choose the runner based on the environment:
In a script:
In a notebook cell:
This pattern keeps the business logic reusable and moves the environment-specific entry point out of the coroutine.
Detecting the Environment Programmatically
If you need a single entry point that works in both contexts:
This works but adds complexity. For library authors, it is better to expose the coroutine directly and let callers decide how to run it.
Debugging Library-Caused Loop Conflicts
Some notebook errors appear only after calling a third-party library that internally uses asyncio. Common culprits include HTTP client libraries, database drivers, and message queue clients.
The debugging process:
- Check if the library exposes an async/awaitable API alongside the sync one
- If it does, use the async API with
awaitinstead of wrapping the sync API withasyncio.run() - If it only has a sync API that internally uses
asyncio.run(), applynest_asynciobefore importing the library
Comparison of Solutions
| Approach | Complexity | When to Use |
Top-level await | Low | Default choice for all notebook async code |
asyncio.create_task() | Low | Concurrent background work in notebooks |
nest_asyncio | Medium | Third-party code that calls asyncio.run() internally |
| Environment detection | High | Libraries that must work everywhere without modification |
| Refactoring to coroutines | Medium | Long-term solution for portable async code |
Common Pitfalls
Calling asyncio.run() inside a notebook cell because a tutorial or Stack Overflow answer used it in a script context is the most common trigger. Tutorials written for scripts are not directly portable to notebooks.
Using nest_asyncio as the default solution when top-level await would be simpler and cleaner adds an unnecessary dependency and hides the real issue.
Mixing notebook-style await and manual loop control (loop.run_until_complete()) in the same session can cause subtle ordering bugs and unexpected behavior.
Assuming the error means the coroutine itself is broken leads to wasted debugging time. The coroutine is usually fine. The problem is how it is being invoked.
Forgetting that asyncio.create_task() requires an already-running loop to work means it functions perfectly in notebooks but fails in plain scripts without asyncio.run() wrapping it.
Summary
- Jupyter already runs an event loop, so
asyncio.run()conflicts with it by trying to create a second one. - In notebooks, use top-level
awaitto run coroutines. This is the simplest and most Pythonic solution. - Use
asyncio.create_task()when you need concurrent work within a notebook session. - Reserve
nest_asynciofor compatibility with third-party libraries that internally callasyncio.run(). - Keep async business logic in coroutines and vary only the entry point between scripts and notebooks.
- When a library causes the error, check whether it offers an async API you can
awaitdirectly.

