asyncio.run() — The Event Loop Entry Point
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.
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?
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())
The Output
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
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
asyncio.run() within an already running event loop, leading to RuntimeError.