Event Loop
The Event Loop — The Complete Guide
Understand the event loop: the engine behind async programming that lets a single thread handle thousands of tasks without blocking.
Introduction
The event loop is the core mechanism that powers asynchronous programming. It is a continuously running loop that waits for events (like a network response, a timer firing, or a file finishing reading) and then runs the callbacks or coroutines associated with them. Instead of blocking a thread while waiting, the event loop lets that thread do other useful work, making it possible to handle many concurrent operations efficiently on a single thread.
Real-World Analogy
The Hotel Receptionist
A single receptionist (one thread) handles many guests. They start one guest's room booking, and while the system processes it, they help the next guest instead of waiting. When the first booking is confirmed, they return to it. The event loop is that receptionist — always working on whatever is ready, never frozen waiting on one task.
Prerequisites
Before You Start
- Understanding of functions, callbacks, and control flow
- Basic idea of blocking vs non-blocking I/O
- Familiarity with
async/await(helpful) - At least one runtime installed (Node.js or Python 3.7+)
- Awareness of the call stack and queues (helpful, not required)
How the Event Loop Works
- Code runs on the call stack until it hits an async operation (I/O, timer, network).
- The async operation is handed off to the runtime/OS and the thread keeps going — it does not wait.
- When that operation completes, its callback/continuation is placed into a queue.
- The event loop checks: "Is the call stack empty?" If yes, it takes the next item from the queue.
- That callback runs, possibly starting more async work, and the cycle repeats — forever, until there's nothing left.
Core Components
Call Stack
Where synchronous code executes.
Functions are pushed when called and popped when they return. The event loop only pulls new work when this stack is empty.
Task / Callback Queue
Where ready callbacks wait their turn.
Completed async operations place their callbacks here (macrotasks). The loop processes them one at a time in order.
Microtask Queue
High-priority queue for promises.
Promise callbacks (.then, await continuations) run here, and it is fully drained before the next macrotask.
Event Loop in JavaScript
JavaScript is single-threaded, and its event loop coordinates the call stack, microtasks, and macrotasks.
console.log("1: Start");
setTimeout(() => {
console.log("4: Timeout (macrotask)");
}, 0);
Promise.resolve().then(() => {
console.log("3: Promise (microtask)");
});
console.log("2: End");
// Output order:
// 1: Start
// 2: End
// 3: Promise (microtask) <- microtasks run before macrotasks
// 4: Timeout (macrotask)
0ms timeout, the Promise runs first because the microtask queue is drained before any macrotask.
Event Loop in Python (asyncio)
In Python, asyncio provides the event loop. asyncio.run() creates it, runs your coroutine, and closes it.
import asyncio
async def say(msg, delay):
await asyncio.sleep(delay) # yields control back to the loop
print(msg)
async def main():
# The loop interleaves these while each one "sleeps"
await asyncio.gather(
say("World", 2),
say("Hello", 1),
)
asyncio.run(main())
# Output:
# Hello (after 1s)
# World (after 2s)
Accessing the loop directly
import asyncio
async def main():
loop = asyncio.get_running_loop()
print("Loop is running:", loop.is_running())
# Schedule a plain function on the loop
loop.call_soon(lambda: print("Called soon!"))
await asyncio.sleep(0) # let the loop process it
asyncio.run(main())
Microtasks vs Macrotasks
| Type | Examples | Priority |
|---|---|---|
| Microtask | Promise.then, await continuations, queueMicrotask |
Higher — drained fully before each macrotask |
| Macrotask | setTimeout, setInterval, I/O callbacks, setImmediate |
Lower — one runs per loop iteration |
Node.js Event Loop Phases
Node.js (built on libuv) runs its event loop through ordered phases on each iteration (a "tick"):
| Phase | What Runs |
|---|---|
| Timers | Callbacks from setTimeout / setInterval |
| Pending Callbacks | Certain deferred system callbacks |
| Poll | Retrieve new I/O events; run I/O callbacks |
| Check | setImmediate callbacks |
| Close Callbacks | Cleanup like socket.on('close') |
Why One Thread Handles So Much
With blocking threads, handling \(N\) concurrent connections needs \(N\) threads, costing memory \(N \times M_t\). An event loop uses one thread and only tracks lightweight callbacks, so its memory scales as:
\[ \text{Mem}_{\text{loop}} = M_{\text{base}} + N \times M_{cb} \qquad\text{where}\qquad M_{cb} \ll M_t \]
Since a callback footprint \(M_{cb}\) is tiny compared to a thread stack \(M_t\), one event loop can serve tens of thousands of connections.
The Golden Rule: Never Block the Loop
time.sleep() directly in a callback/coroutine — the whole event loop halts.
await asyncio.sleep().
Best Practices
Do This
- Keep callbacks short — return control to the loop quickly
- Never run blocking or heavy CPU code on the loop thread
- Offload CPU-bound work to threads/processes or a worker pool
- Use non-blocking, async I/O libraries throughout
- Be aware microtasks run before the next macrotask
- Add timeouts so a stuck operation can't hog the loop
Common Mistakes
setTimeout(fn, 0) runs immediately — it waits until the current stack and all microtasks are done.
0ms means "as soon as the loop is free," not "right now."
Interview Questions
| Question | Short Answer |
|---|---|
| What is the event loop? | A loop that runs ready callbacks/coroutines when the call stack is empty, enabling non-blocking concurrency. |
| Microtask vs macrotask? | Microtasks (promises) run before macrotasks (timers/I/O); the microtask queue is drained first. |
Why does setTimeout(fn, 0) not run instantly? |
It waits for the stack to clear and all microtasks to finish before its macrotask runs. |
| How does one thread handle many connections? | By never blocking — it switches to whatever callback is ready instead of waiting. |
| What happens if you block the loop? | All other tasks freeze until the blocking code finishes; responsiveness dies. |
Quick Revision
| Concept | One-Line Summary |
|---|---|
| Call Stack | Runs synchronous code; loop waits for it to empty |
| Microtask Queue | Promise/await callbacks — highest priority |
| Macrotask Queue | Timers and I/O callbacks — one per tick |
| Event Loop | Moves ready callbacks onto the stack, forever |
| Golden Rule | Never block the loop |
Key Takeaways
The event loop is the engine of async programming: it runs ready work whenever the call stack is empty, prioritizing microtasks over macrotasks. It lets a single thread serve huge concurrency — as long as you follow the golden rule and never block it.