Table of Contents

    Event Loop

    CONCURRENCY & RUNTIME

    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.

    In one line: The event loop is a loop that keeps asking "what's ready to run now?" and runs it — never sitting idle while waiting.

    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

    1. Code runs on the call stack until it hits an async operation (I/O, timer, network).
    2. The async operation is handed off to the runtime/OS and the thread keeps going — it does not wait.
    3. When that operation completes, its callback/continuation is placed into a queue.
    4. The event loop checks: "Is the call stack empty?" If yes, it takes the next item from the queue.
    5. That callback runs, possibly starting more async work, and the cycle repeats — forever, until there's nothing left.

    Core Components

    1

    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.

    2

    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.

    3

    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)
    Key insight Even with a 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

    CRITICAL RULE
    One blocked callback freezes every other task
    Bad Running a heavy CPU loop or time.sleep() directly in a callback/coroutine — the whole event loop halts.
    Good Offload CPU work to a worker thread/process, and use non-blocking waits like 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

    Bad Assuming setTimeout(fn, 0) runs immediately — it waits until the current stack and all microtasks are done.
    Good Understanding that 0ms means "as soon as the loop is free," not "right now."
    Bad Creating an infinite synchronous loop or huge computation on the main thread — it starves all pending callbacks.

    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.