Ali Raza

September 9, 2026

A Tale of Two Loops: The JavaScript Event Loop in Browsers vs. Node.js

JavaScript is famously single-threaded, meaning it can only execute one piece of JavaScript at a time.

Yet, we can:

  • Fetch data from an API
  • Read files
  • Listen for user interactions
  • Handle network requests
  • Run timers

without freezing the application.

The secret behind this non-blocking behavior is the Event Loop.

However, while both browsers and Node.js use an event loop to orchestrate asynchronous operations, their implementations are designed for fundamentally different environments.


The Browser Event Loop: The UI Juggler

A browser's primary job is to keep the user interface smooth and responsive.

Its event loop continuously coordinates JavaScript execution, asynchronous browser APIs, user interactions, and rendering.

Web APIs

Browsers provide APIs that JavaScript can interact with, such as:

  • DOM
  • fetch()
  • setTimeout()
  • addEventListener()
  • requestAnimationFrame()

These APIs are provided by the browser environment, not by the JavaScript language itself.

Microtask and Macrotask Queues

The browser event loop works with different categories of tasks.

Microtask Queue

Microtasks have a higher priority and are processed after the current JavaScript execution finishes.

Common examples include:

  • Promise callbacks
  • queueMicrotask()
  • MutationObserver
Promise.resolve().then(() => {
  console.log("Microtask");
});

Macrotask Queue

Tasks such as these are scheduled as regular tasks:

  • setTimeout()
  • setInterval()
  • User interaction events
  • Some browser events
setTimeout(() => {
  console.log("Macrotask");
}, 0);

Rendering

Unlike Node.js, the browser also has another major responsibility:

Keeping the screen visually responsive.

The browser periodically performs rendering work such as:

  1. Style calculation
  2. Layout
  3. Paint
  4. Compositing

Animations can also be coordinated using:

requestAnimationFrame();

The event loop therefore has to balance JavaScript execution, asynchronous callbacks, user interactions, and rendering.


The Node.js Event Loop: The I/O Master

Node.js was designed primarily for server-side workloads.

There is no DOM and no screen that needs to be rendered.

Instead, Node.js is optimized for handling large numbers of concurrent operations such as:

  • Network requests
  • Database queries
  • File-system operations
  • TCP connections
  • Timers

Powered by libuv

Node.js uses a C library called libuv to provide its asynchronous I/O infrastructure.

libuv interacts with the operating system and manages things such as:

  • Event-loop execution
  • Asynchronous I/O
  • Timers
  • Networking
  • Thread-pool operations

This allows Node.js to handle many I/O operations without blocking the JavaScript thread.


Node.js Event Loop Phases

Unlike the simplified browser model, Node.js organizes its event loop into several distinct phases.

The major phases are:

  1. Timers

    • Executes callbacks scheduled by setTimeout() and setInterval().
  2. Pending Callbacks

    • Executes certain callbacks deferred from previous operations.
  3. Idle / Prepare

    • Internal libuv operations.
  4. Poll

    • Retrieves and processes I/O events.
    • Can wait for new I/O when appropriate.
  5. Check

    • Executes callbacks scheduled by setImmediate().
  6. Close Callbacks

    • Executes callbacks associated with closed handles, such as sockets.

The simplified order is:

Timers
   ↓
Pending Callbacks
   ↓
Idle / Prepare
   ↓
Poll
   ↓
Check
   ↓
Close Callbacks
   ↓
Back to Timers

The Poll Phase: Node.js's Waiting Point

One of the important characteristics of Node.js is the Poll phase.

When Node.js reaches the Poll phase, it processes available I/O events.

If there is no immediate work to process, Node.js can wait for new I/O events rather than continuously consuming CPU cycles.

For example:

              ┌─────────────┐
              │    Timers   │
              └──────┬──────┘
                     ↓
              ┌─────────────┐
              │   Pending   │
              │  Callbacks  │
              └──────┬──────┘
                     ↓
              ┌─────────────┐
              │    Poll     │
              │             │
              │ Wait for I/O│
              └──────┬──────┘
                     ↓
              ┌─────────────┐
              │    Check    │
              │ setImmediate│
              └──────┬──────┘
                     ↓
              ┌─────────────┐
              │    Close    │
              └──────┬──────┘
                     │
                     └──────→ Timers

This behavior makes Node.js well suited for I/O-heavy server applications.


Key Differences at a Glance

| Feature | Browser | Node.js | | --- | --- | --- | | Primary Goal | UI responsiveness | High-concurrency I/O | | Environment APIs | Web APIs, DOM, Fetch | Node.js APIs, libuv | | Rendering | Yes | No | | Event Loop Model | Tasks + microtasks + rendering | Multiple libuv phases | | I/O Handling | Browser-managed Web APIs | libuv + OS facilities | | Waiting Behavior | Coordinates with rendering and browser tasks | Can wait in the Poll phase for I/O | | Unique APIs | requestAnimationFrame() | process.nextTick(), setImmediate() |


The Big Picture

The fundamental idea is the same in both environments:

JavaScript
    ↓
Start asynchronous operation
    ↓
Environment handles the operation
    ↓
Operation completes
    ↓
Callback becomes eligible to run
    ↓
Event Loop schedules JavaScript

But the environments have different priorities.

Browser

JavaScript
    +
Web APIs
    +
User Events
    +
Microtasks
    +
Rendering
    ↓
Responsive UI

Node.js

JavaScript
    +
Node APIs
    +
libuv
    +
Operating System
    +
I/O
    ↓
High-concurrency server

So, while the core idea of the event loop is shared, the implementation is tailored to the environment.

Browsers are designed around responsiveness and rendering, while Node.js is designed around efficient server-side I/O.