ReviseAlgo Logo

Concurrency

Event Loops

The asyncio event loop internals — how the loop schedules callbacks, manages tasks, and orchestrates concurrent execution.

Interview: Deep understanding of async — shows knowledge of how asyncio actually works under the hood.

Last Updated: June 12, 2026 8 min read

The event loop is the heart of asyncio. It's a program construct that runs in a single thread, dispatching callbacks and coroutines. When a coroutine reaches an await point, the event loop takes back control and runs other ready tasks. Understanding the event loop helps debug timing issues and write efficient async code.

How the Event Loop Works

  • Ready queue: Tasks that can run immediately (not waiting on I/O)
  • I/O polling: The loop checks which I/O operations have completed (using epoll/kqueue/select)
  • Timer heap: Scheduled callbacks (from call_later, sleep) sorted by time
  • Each iteration: process ready tasks → poll I/O → process expired timers → repeat

Event Loop API

  • asyncio.run(coro) — creates a new loop, runs the coroutine, closes the loop (preferred entry point)
  • asyncio.get_event_loop() — get the current loop (deprecated in 3.10+, use get_running_loop())
  • asyncio.get_running_loop() — get the currently running loop (from inside a coroutine)
  • loop.run_in_executor(executor, func) — run blocking code in a thread/process pool from async context

Scheduling Callbacks

  • loop.call_soon(callback) — schedule callback for next iteration
  • loop.call_later(delay, callback) — schedule after a delay
  • loop.call_at(when, callback) — schedule at an absolute time
  • These are low-level — prefer asyncio.create_task() for most use cases

Interview Insight

The event loop is single-threaded — it can only run one callback at a time. Blocking the loop (with CPU-heavy work or blocking I/O) starves all other tasks. Use run_in_executor() to offload blocking work to threads while keeping the loop responsive.

Common Pitfall

Never create multiple event loops in the same thread. asyncio.run() creates and closes a loop — calling it twice in the same context can cause "Event loop is closed" errors. For long-running apps, use loop.run_forever().

Use Cases

Integrating blocking libraries — run_in_executor for requests, psycopg2, etc.

Custom scheduling — delayed callbacks, periodic tasks

Graceful shutdown — timeout + cancel pattern for clean application exit

Background work — shield() to protect critical operations from cancellation

Server applications — long-running event loops with lifecycle management

Common Mistakes

Calling asyncio.run() inside an async function — it creates a new loop, causing errors

Blocking the event loop with CPU-heavy work — use run_in_executor instead

Not closing the event loop — resource leaks in long-running applications

Using get_event_loop() in Python 3.10+ — prefer get_running_loop() inside coroutines

Forgetting that the loop runs one task at a time — any blocking call stalls everything