Introduction
Node.js runs JavaScript on a single main thread, yet it can handle thousands of concurrent connections without blocking. This is possible because of its event-driven architecture: expensive I/O operations (reading files, network requests, database queries) are offloaded to the operating system or a background thread pool, while the main thread stays free to keep executing code. The event loop is the mechanism that coordinates this — it continuously checks whether there is work to do and dispatches callbacks when asynchronous operations complete.
Cricket analogy: A single umpire can officiate an entire match without stopping play by delegating ball-tracking to Hawk-Eye and DRS reviews to the third umpire, freeing them to keep the game moving, much like Node offloading I/O to the OS.
How It Works
At a high level, Node.js architecture has three layers: your JavaScript code, the Node.js APIs/bindings (fs, http, etc.), and libuv, which manages the event loop and a thread pool for operations that cannot be done asynchronously at the OS level (like some file system calls). The call stack executes synchronous code first. Asynchronous operations are registered with libuv or the OS, and when they complete, their callbacks are placed into queues. The event loop repeatedly checks the call stack: if it's empty, it pulls callbacks from these queues and pushes them onto the stack to execute.
Cricket analogy: The captain (JavaScript code) gives instructions, the team management staff (Node APIs) translate them into ground operations, and the groundskeeper (libuv) manages the actual pitch prep and player rotations behind the scenes.
The event loop has distinct phases, executed in order on each iteration ('tick'): timers (executes callbacks scheduled by setTimeout/setInterval), pending callbacks (I/O callbacks deferred from the previous cycle), poll (retrieves new I/O events and executes their callbacks; this is where most I/O callbacks run), check (executes setImmediate callbacks), and close callbacks (e.g. socket.on('close', ...)). Between each phase, and after each callback on the call stack finishes, Node.js drains the microtask queue, which holds Promise callbacks (.then/.catch/.finally) and process.nextTick callbacks. process.nextTick callbacks run before other microtasks (Promises), and both run before the event loop proceeds to the next phase.
Cricket analogy: An over goes through fixed phases: bowler's run-up (timers), umpire signals from last over (pending callbacks), the actual delivery and shot (poll), instant replay checks (check), and the fielder returning the ball (close) — with DRS reviews (microtasks) squeezed in between each phase.
Example
console.log('1: start');
setTimeout(() => console.log('2: setTimeout'), 0);
setImmediate(() => console.log('3: setImmediate'));
Promise.resolve().then(() => console.log('4: promise microtask'));
process.nextTick(() => console.log('5: nextTick'));
console.log('6: end');Output
1: start
6: end
5: nextTick
4: promise microtask
2: setTimeout
3: setImmediateExplanation
Synchronous code (lines 1 and 6) runs first because the call stack is processed before any queue. Once the call stack is empty, process.nextTick callbacks run first, followed by the Promise microtask queue — both are drained completely before the event loop moves into its phases. Then the event loop enters the timers phase, running the setTimeout callback, and later the check phase runs the setImmediate callback. The exact ordering of setTimeout(0) versus setImmediate can vary when called from the main module, but inside an I/O callback, setImmediate always fires before setTimeout.
Cricket analogy: The toss and team announcement (sync code) happen first; then any urgent DRS review from the previous ball (nextTick/microtasks) is resolved before the umpire moves to signaling overs (timers) and eventually to boundary review checks (setImmediate) at the innings break.
Key Takeaways
- Node.js runs JavaScript on a single main thread but delegates I/O to the OS and a libuv thread pool.
- The event loop has ordered phases: timers, pending callbacks, poll, check, and close callbacks.
- process.nextTick and Promise microtasks are drained after every callback, before the loop moves to the next phase.
- process.nextTick callbacks always run before Promise microtasks.
- Blocking the call stack with heavy synchronous computation freezes the entire event loop.
Practice what you learned
1. What executes asynchronous I/O operations in Node.js's architecture?
2. In which event loop phase do most I/O callbacks execute?
3. Between process.nextTick() and a Promise .then() callback scheduled at the same time, which runs first?
4. What happens if you run a CPU-intensive synchronous loop in a Node.js server?
Was this page helpful?
You May Also Like
Introduction to Node.js
Learn what Node.js is, why it exists, and how it lets JavaScript run outside the browser on servers.
Callbacks in Node.js
How Node.js uses callback functions to handle asynchronous operations without blocking the main thread.
Promises in Node.js
Understanding the Promise object and its pending, fulfilled, and rejected states for cleaner async code.
Async/Await in Node.js
Writing asynchronous code that reads like synchronous code using the async/await syntax built on Promises.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics