Node.js Event Loop & Asynchronous Execution — Clearly Explained

By Abhijeet Verma · · Node.js Fundamentals

Understanding the Node.js event loop is the difference between “it works” and “I truly understand what’s happening.”

This blog builds a clear mental model — no myths, no hand-waving.


1. The Big Picture: What Actually Runs Your Code?

Node.js has two core pieces:

V8 (JavaScript Engine)

  • Executes JavaScript

  • Manages:

    • Call stack
    • Heap
    • Promise microtasks
  • Runs code to completion

  • Single-threaded

libuv (C library)

  • Implements the event loop

  • Handles:

    • Timers
    • I/O polling
    • Thread pool
  • Selects which callback runs next

  • Never executes JS

V8 executes JavaScript. libuv schedules callbacks.


2. The Call Stack — Where JS Actually Runs

All JavaScript runs on a single thread.

console.log("start");
while(true) {}
console.log("end");

If that loop runs forever:

  • Nothing else executes.
  • No timers.
  • No I/O.
  • No promises.

Because:

JavaScript runs to completion. Nothing interrupts it.


3. Top-Level Code vs Event Loop

This is widely misunderstood.

console.log("start");
setTimeout(() => console.log("timeout"));
console.log("end");

What happens?

  1. Script loads.
  2. All synchronous code runs immediately.
  3. setTimeout only registers a callback.
  4. Only when the stack is empty does the event loop start iterating.

🔑 Important Insight

The event loop runs callbacks. Top-level code is NOT a callback.


4. Microtasks — The Highest Priority Work

Node.js has two microtask queues:

  1. process.nextTick() (Node-specific, highest priority)
  2. Promise microtasks (.then, await)

Drain Rule (Critical)

After every callback execution:

  1. Drain process.nextTick
  2. Drain Promise microtasks
  3. Then continue the event loop

Visual Flow

flowchart TD
    A[libuv selects callback]
    B[V8 executes callback]
    C[Drain microtasks]
    D[Return to libuv]

    A --> B --> C --> D --> A

libuv chooses. V8 executes. Microtasks drain. Repeat forever.

Consequences

  • Microtasks run before timers and I/O.
  • Too many microtasks can starve the event loop.
  • process.nextTick can freeze everything if abused.

5. async / await — What It REALLY Does

await something();
console.log("after");

This means:

  • Pause function
  • Schedule the rest as a Promise microtask

await never blocks. It splits execution.

Each await = One Microtask Hop

Multiple awaits = multiple microtask drains.

Heavy chaining can delay timers and I/O.


6. Event Loop Phases (Node-Specific)

Node’s event loop has fixed phases:

flowchart LR
    T[Timers]
    P1[Pending Callbacks]
    I[Idle / Prepare]
    POLL[Poll]
    C[Check]
    CL[Close Callbacks]

    T --> P1 --> I --> POLL --> C --> CL --> T

6.1 Timers Phase

  • setTimeout
  • setInterval
  • Delay is minimum, not exact

6.2 Pending Callbacks

  • System-level callbacks
  • Mostly I/O error-related

6.3 Idle / Prepare

  • Internal
  • No user JS runs
  • Prepares for Poll (decides how long the Poll phase can block based on nearest timer deadline)

6.4 Poll (The Heart of Node.js)

Handles:

  • I/O callbacks (fs, net, http)

  • Decides:

    • Block?
    • Continue?
    • Move to next phase?

Poll balances latency vs throughput.


6.5 Check Phase

  • Runs setImmediate
  • Always after Poll

6.6 Close Callbacks

  • Cleanup handlers
  • Socket close events

7. setImmediate vs setTimeout(0)

  • setTimeout(0) → Timers phase
  • setImmediate() → Check phase

Inside I/O:

Poll finishes → Check runs next So setImmediate often runs first.


8. Offloading Work: Kernel vs Thread Pool

Node uses two async mechanisms.

A. OS-Level Async I/O (Best Case)

Used for:

  • TCP
  • HTTP
  • UDP

Mechanism:

  • Registers descriptors via epoll / kqueue
  • No threads blocked
  • OS signals readiness

B. libuv Thread Pool

Used for:

  • fs
  • crypto
  • zlib
  • DNS lookup

Default size: 4 threads Configurable via:

UV_THREADPOOL_SIZE=8 node app.js

Visual Flow

flowchart TD
    JS[V8 JS Code]
    API[Node API call]
    UV[libuv]
    OS[OS Kernel epoll]
    TP[Thread Pool]
    POLL[Poll Phase]
    CB[Callback Execution]

    JS --> API --> UV
    UV --> OS
    UV --> TP
    OS --> POLL
    TP --> POLL
    POLL --> CB

Performance Insight

If you see:

  • High CPU
  • Slow I/O
  • Low JS usage

You are likely thread pool bound, not event-loop bound.


9. Starvation Scenarios

Microtask Starvation

function loop() {
  Promise.resolve().then(loop);
}
loop();

Event loop never progresses.


nextTick Starvation

Even worse:

function loop() {
  process.nextTick(loop);
}
loop();

I/O completely freezes.


10. One-Page Mental Model

flowchart TD
    STACK[JS Call Stack]
    NT[nextTick queue]
    PM[Promise microtasks]
    EL[Event Loop selects one callback]

    STACK --> NT --> PM --> EL --> STACK

Repeat forever.


12. Code Challenge #1

setTimeout(() => console.log("timeout"));

Promise.resolve().then(() => console.log("promise"));

process.nextTick(() => console.log("nextTick"));

console.log("sync");

Output:

sync
nextTick
promise
timeout

Why?

  1. Sync runs first.
  2. nextTick drains.
  3. Promise microtask drains.
  4. Timers phase runs.

13. Code Challenge #2

setImmediate(() => console.log("immediate"));

fs.readFile(__filename, () => {
  console.log("file");
});

Output:

file
immediate

Why?

  1. fs.readFile completes.
  2. Callback queued in Poll.
  3. Poll runs → prints "file".
  4. Check phase runs → prints "immediate".

14. Node.js vs Browser Architecture

Aspect Node.js Browser
Primary role Server runtime UI runtime
Async provider libuv Web APIs
Event loop Multi-phase Task-based
File system ✅ Yes ❌ No
DOM ❌ No ✅ Yes
Rendering ❌ None ✅ Core feature

JavaScript is the same. The environment defines the power.


15. Promises vs Callbacks (The Confusion Point)

Callback Style

fs.readFile("file.txt", () => {
  console.log("file");
});
  • I/O completes
  • Callback queued in Poll
  • Not a microtask

Promise Style

await fs.promises.readFile("file.txt");
console.log("after");

Split into two parts:

  1. File read (Poll phase)
  2. Promise resolves → schedules microtask

So:

  • File I/O → Poll callback
  • Code after await → microtask

🔥 Final Truths to Remember

  • Event loop schedules callbacks.
  • V8 executes JavaScript.
  • Microtasks run between callbacks.
  • Poll is the heart of Node.
  • await never blocks.
  • Single-threaded ≠ slow.
  • Blocking JS blocks everything.

If you understand this model deeply, you no longer “guess” Node.js behavior — you can predict it.

That’s the real goal.