← Python Code Async Python
Browse Python Concepts

asyncio.run() — The Event Loop Entry Point

Mental Model

Think of asyncio.run() as the single, main switch that powers on the entire asynchronous machinery for your application. You can only flip this switch once to start the event loop; you cannot try to flip it again while it's already running within the same thread.

Rule: Never call asyncio.run() from inside a running event loop or any asynchronous context.

The Setup

A backend engineer attempts to integrate an async caching library into a synchronous web framework handler by calling asyncio.run() on every incoming request.

What Does This Print?

Broken code
Python
import asyncio

async def fetch_cache(key):
    await asyncio.sleep(0.01)
    return f"Value for {key}"

def handle_request(key):
    # Spin up a new event loop on every incoming request
    return asyncio.run(fetch_cache(key))

async def run_server():
    # Simulation of web server handling concurrent requests
    for i in range(3):
        data = handle_request(f"key_{i}")
        print(f"Handled: {data}")

# Supposing the main loop is already running
asyncio.run(run_server())
Predict what exception is raised when executing this script.

The Output

What actually happens
RuntimeError: asyncio.run() cannot be called from a running event loop

The code crashes with a RuntimeError. You cannot call asyncio.run() while another event loop is already executing on the current thread. In this example, run_server() is executed within an event loop managed by the top-level asyncio.run(run_server()). When handle_request calls asyncio.run(fetch_cache(key)), it attempts to start a nested event loop on the same thread, which Python strictly prohibits.

Why Python Does This

asyncio.run() is a high-level API introduced in Python 3.7. It performs a specific set of operations: it creates a new event loop, sets it as the current event loop for the thread, executes the passed coroutine, finalizes asynchronous generators, shuts down thread pools, and closes the loop. It is fundamentally designed to be the main entry point of your program. The underlying CPython event loop scheduler is single-threaded and relies on global thread-local state to track the active event loop. Allowing nested loops would corrupt the execution state of the outer loop, leading to unresolved futures and race conditions within the single-threaded scheduler.

The Fix

Corrected pattern
Python
import asyncio

async def fetch_cache(key):
    await asyncio.sleep(0.01)
    return f"Value for {key}"

# Make the request handler async to participate in the existing loop
async def handle_request(key):
    return await fetch_cache(key)

async def run_server():
    for i in range(3):
        # Correctly await the handler, avoiding nested loops
        data = await handle_request(f"key_{i}")
        print(f"Handled: {data}")

if __name__ == "__main__":
    # Single entry point to start the event loop
    asyncio.run(run_server())

asyncio.run() is designed to be the top-level entry point for your asynchronous program, managing the lifecycle of the event loop. When inside an existing async function, you are already within an active event loop, so you should use await or asyncio.create_task() to run other coroutines instead of starting a new loop.

How This Fails in Real Systems

An enterprise Django application migrated its payment processing flow to an async SDK. To avoid rewriting synchronous views, developers placed asyncio.run(process_payment()) directly inside a synchronous Django view. During a marketing promotion, concurrent requests caused thread-pool exhaustion and multiple nested event loop failures, taking down the entire checkout service for over an hour.

Key Takeaway

Never call asyncio.run() from inside a running event loop or any asynchronous context.
Common mistake: Developers attempt to encapsulate synchronous-looking asynchronous calls by wrapping them in asyncio.run() within an already running event loop, leading to RuntimeError.