Node.jsArchitectureAdvancedDeep Dive

    Visualizing the Node.js Event Loop Guide

    Jan 18, 202610 min read

    How does single-threaded Node.js handle millions of requests? Deep dive into the phases of the Event Loop.

    The Single-Threaded Secret#

    Node.js is famous for being single-threaded, yet it powers some of the highest-traffic applications in the world (like Netflix and Uber). How can a single thread handle millions of concurrent connections without freezing?

    The answer lies in the Reactor Pattern, implemented via the Event Loop.

    Understanding the Event Loop is the difference between writing a Node.js app that is fast and scalable, and one that crashes under load.


    The Architecture: Libuv#

    Node.js is essentially a wrapper around two key components:

    1. V8 Engine: Executes your JavaScript code (synchronously).
    2. Libuv: A C library that handles the Event Loop, Thread Pool, and all asynchronous I/O (file system, network, DNS).

    When you write fs.readFile() in JavaScript, V8 hands the request off to Libuv and continues executing the next line of code immediately. Libuv does the heavy lifting in the background (using OS threads) and places a "callback" in a queue when it's done.


    The 6 Phases of the Event Loop#

    The Event Loop isn't just one big queue. It acts like a spinning carousel with 6 distinct stops (phases). In each "tick" (rotation), Node.js visits these phases in order:

    1. Timers Phase

    This is the first shop. Node checks: "Are there any setTimeout or setInterval callbacks that are ready to run?" If a timer was set for 100ms and 100ms has passed, the callback is executed here.

    2. Pending Callbacks Phase

    This phase executes I/O callbacks that were deferred from the previous loop iteration. It handles system-level operations like TCP errors (e.g., ECONNREFUSED).

    3. Idle, Prepare Phase

    This is internal use only. You can ignore this conceptually, but know that Node is preparing for the heavy lifting.

    4. Poll Phase (The Most Important Phase)

    This is where 90% of the magic happens.

    • Node calculates how long it should block/wait for I/O.
    • It processes events in the Poll Queue (incoming data, file read complete).
    • If the queue is empty, it might wait here for new I/O events, effectively putting the CPU to sleep to save power properly.

    5. Check Phase

    This is where setImmediate() callbacks run.

    • Why use setImmediate? Use it when you want to execute code immediately after any I/O operation finishes, ensuring it runs before any timers.

    6. Close Callbacks Phase

    Cleanup time. Callbacks for socket.on('close', ...) run here.


    The "Microtask" Queues: The Cutters#

    There are two special queues that don't belong to any phase. They are the VIPs that cut the line. These queues are drained completely after every single operation and between phases.

    1. process.nextTick() Queue: Has the highest priority. If you call process.nextTick(), it runs immediately after the current operation finishes, before anything else.
    2. Promise Microtask Queue: Handles Promise.resolve() and async/await.

    Warning: If you recursively call process.nextTick(), you will block the Event Loop completely (starvation), and I/O will never happen.


    Common Mistakes: Blocking the Event Loop#

    Since there is only one thread for JavaScript execution, if you run a heavy calculation, everything stops.

    The "JSON Attack"

    // BAD: This blocks the entire server!
    app.post('/data', (req, res) => {
        const data = fs.readFileSync('huge-file.json'); // Blocking I/O
        const json = JSON.parse(data); // CPU Intensive parsing
        res.send('Done');
    });
    

    while JSON.parse is running on a 50MB file, no other user can connect to your server.

    The Solution: Worker Threads

    For CPU-intensive tasks (image processing, crypto, huge JSON), do not use the Event Loop. Use Worker Threads.

    const { Worker } = require('worker_threads');
    
    app.post('/process-image', (req, res) => {
        const worker = new Worker('./worker-script.js');
        worker.postMessage(req.body.image);
        
        worker.on('message', (result) => {
            res.send(result);
        });
    });
    

    Summary#

    1. Synchronous Code runs first (Stack).
    2. Microtasks (nextTick, Promises) run next.
    3. Event Loop Phases run in a cycle (Timers -> Poll -> Check).
    4. Never block the main thread with heavy CPU tasks; offload them or use Worker Threads.

    Ready to try it yourself?

    Experience the power of WebAssembly and Node.js directly in your browser. No setup required.