Unlike many other environments where each request might spawn a new thread, Node.js operates primarily on a single main thread. This sounds limiting, but it achieves high concurrency and performance through an **event-driven, non-blocking I/O model**, powered by the **Event Loop**.
Think of the Event Loop as a constantly running process that orchestrates tasks. When Node.js starts, it initializes the loop, processes the input script (which might register async operations), and then enters the loop. The loop's job is to check for pending events (like completed I/O, timers) and execute their associated callback functions.
The key is non-blocking I/O. Instead of waiting for slow operations (like reading a file or making a network request), Node.js delegates these tasks to the operating system or a worker thread pool (managed by the libuv library). When the operation completes, the OS/libuv notifies Node.js by placing the corresponding callback function into a queue. The Event Loop picks up these callbacks from the queue and executes them on the main thread when it gets a chance.
setTimeout
, fs.readFile
, http.request
, etc.). When you call these, Node.js typically hands the work off to libuv or the OS kernel. These operate outside the main JS thread.setTimeout
, setInterval
), completed I/O operations, and setImmediate
land here. The Event Loop processes this queue during specific phases.process.nextTick
and Promise resolutions (.then()
, .catch()
, .finally()
) go here. The Microtask Queue is processed immediately after the current synchronous code finishes, after each callback from the Callback Queue executes, and between phases of the Event Loop.The Event Loop cycles through a series of phases. In each phase, it processes callbacks specific to that phase.
┌───────────────────────────┐ ┌─>│ timers │<-- Check for expired setTimeout/setInterval callbacks │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │<-- I/O callbacks deferred to the next loop iteration (e.g., certain OS events) │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │<-- Internal use only │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ Incoming: │ │ │ poll │<─────┤ connections, │<-- Calculate block time, process I/O callbacks, execute most Macrotasks │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │<-- Execute setImmediate callbacks │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │<-- Execute socket.on('close'), etc. └───────────────────────────┘ *** Microtask Queue processing happens after each callback and between phases! ***
setTimeout()
and setInterval()
that have reached their threshold.fs.readFile
).setImmediate()
, the loop moves to the check phase.setImmediate
callbacks exist, the loop will wait for callbacks to be added to the queue and execute them immediately.setImmediate()
. These run immediately after the poll phase completes.socket.on('close', ...)
.Microtask Processing: After executing *any* callback (from timers, I/O, check, close phases) or after the initial synchronous script finishes, Node.js immediately checks and executes *all* callbacks currently in the Microtask Queue (first process.nextTick
, then Promises) before moving on or starting the next loop iteration. This gives microtasks higher priority and allows them to run before other queued macrotasks.
Add asynchronous tasks and step through the simulation to see how the Event Loop processes them.