⬡ Node.js

Understanding the
Event Loop

📅 Jan 2025 ⏱ 8 min read 🏷

The event loop is what allows Node.js to perform non-blocking I/O despite JavaScript being single-threaded. Here's a deep dive into every phase — and exactly what runs when.

What is the Event Loop?

When you run a Node.js program, it starts executing your script top to bottom. Once the initial execution is done, instead of quitting, Node enters the event loop — a continuous cycle that checks for pending work, processes it, and loops again.

This is what makes Node "non-blocking": while waiting for a file to read or a network response, Node can handle other events instead of sitting idle.

💡

Key insight: JavaScript is single-threaded, but Node uses the OS and libuv's thread pool for I/O. The event loop is the glue that brings results back to your JS code.

The loop has 6 distinct phases, each with its own queue of callbacks. Understanding which phase runs what is the key to mastering async code in Node.js.

Microtasks: The Hidden Layer

⚠️

Watch out: If you recursively call process.nextTick() you can starve the event loop — I/O callbacks will never run because the microtask queue never empties.

Microtask Queue

After each phase, Node drains the entire microtask queue before moving on. process.nextTick() runs first — before Promise callbacks — making it the highest-priority async mechanism in Node.

Microtasks don't belong to any phase — they run between every phase transition. A resolved Promise callback always runs before the loop can move on.

Interactive Demo

Click any phase to explore what runs there. Step through with the Prev / Next buttons.

// Node.js event loop — click a phase to inspect
Event Loop
↻ repeats
① Timers
Executes callbacks scheduled by setTimeout() and setInterval() whose delay has expired.
  • Runs if delay threshold has passed
  • setTimeout(fn,0) still waits until this phase
  • Does NOT guarantee exact timing
② Pending I/O
Handles I/O callbacks deferred from the previous iteration.
  • System-level error callbacks
  • e.g. TCP ECONNREFUSED
  • Most I/O runs in Poll instead
③ Idle/Prepare
Internal Node.js housekeeping. Your JavaScript code never runs here.
  • Internal bookkeeping only
  • Prepares for the Poll phase
  • Not accessible from userland JS
④ Poll
Retrieves and runs I/O callbacks. The loop blocks here if nothing else is pending.
  • Runs completed I/O callbacks
  • Blocks waiting if queue empty + no timers
  • Once timers ready, moves to Timers
⑤ Check
Runs setImmediate() callbacks — always fires right after Poll.
  • setImmediate() runs here
  • Fires before setTimeout(fn,0) inside I/O
  • Predictable timing after Poll
⑥ Close Events
Fires close event callbacks and cleans up before the next loop iteration.
  • socket.on("close") callbacks
  • process.on("exit") fires here
  • Loop repeats or exits after this

The 6 Phases

Each iteration of the loop moves through these phases in order:

Phase 01
⏱ Timers

Runs callbacks scheduled by setTimeout() and setInterval() whose delay threshold has been reached.

setTimeout / setInterval
Phase 02
🔄 Pending I/O

Handles I/O callbacks deferred from the previous loop tick — mostly system-level errors like TCP socket errors.

deferred callbacks
Phase 03
💤 Idle / Prepare

Internal Node.js use only. Prepares state before entering the poll phase. Your code never runs here.

internal only
Phase 04
👂 Poll

Retrieves and executes new I/O events. If the queue is empty and no timers are ready, the loop blocks here waiting.

fs / net / http
Phase 05
✅ Check

Runs setImmediate() callbacks — always fires right after the Poll phase.

setImmediate
Phase 06
🔒 Close Events

Executes close callbacks like socket.on('close') and process.on('exit').

socket.on('close')

TL;DR

text
Each loop iteration:

  1. Timers        → setTimeout / setInterval callbacks
  2. Pending I/O   → deferred system callbacks
  3. Idle/Prepare  → internal, skip
  4. Poll          → I/O events; may block waiting
  5. Check         → setImmediate callbacks
  6. Close         → socket.on('close') etc.

  ⚡ Between EVERY phase: drain microtasks (nextTick, then Promises)

See It in Code

Can you predict the output order before running this snippet?

javascript
// What's the output order?

console.log('1. script start');

setTimeout(() => console.log('4. setTimeout'), 0);
setImmediate(() => console.log('5. setImmediate'));
Promise.resolve().then(() => console.log('3. Promise microtask'));
process.nextTick(() => console.log('2. nextTick'));

console.log('1. script end');

Output: script start → script end → nextTick → Promise → setTimeout → setImmediate. Microtasks always flush before the loop moves on!

setTimeout vs setImmediate vs nextTick

These three are the most commonly confused. Here's a clear comparison:

Function Runs in Priority Use when
process.nextTick() Between any two phases Highest Defer but stay before any I/O
Promise.then() Microtask queue High Standard async/await patterns
setImmediate() Check phase Medium After Poll — inside I/O callbacks
setTimeout(fn, 0) Timers phase Lower Delay by at least N ms

Written for developers who want to understand Node.js deeply.

Node.js JavaScript async event-loop libuv