Understanding the Node.js Event Loop
Understanding the Node.js Event Loop
Node.js runs your JavaScript on a single thread, yet it can handle thousands of concurrent connections without blocking. The event loop is the mechanism that makes that possible — it's what lets Node hand off slow work (file I/O, network requests, timers) and keep running other code while it waits.
The synchronous code runs first
Nothing in the event loop runs until the current synchronous script finishes. This trips people up constantly:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
console.log('end');
// Logs: start, end, timeoutEven with a 0ms delay, setTimeout always runs after the synchronous
code, because the callback is scheduled on the event loop, not executed
immediately.
Microtasks vs macrotasks
The event loop has two queues that matter most in everyday code:
- Microtasks: Promise callbacks (
.then,async/await),queueMicrotask. - Macrotasks:
setTimeout,setInterval, I/O callbacks.
After every single macrotask, Node drains the entire microtask queue before moving on. That means promises consistently jump ahead of timers:
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
// Logs: sync, promise, timeoutThe phases of the loop
Node's event loop (via libuv) cycles through phases in order, each with its own queue:
- Timers —
setTimeout/setIntervalcallbacks whose time has elapsed. - Pending callbacks — some system-level callbacks deferred from the previous cycle.
- Poll — retrieves new I/O events; executes I/O callbacks.
- Check —
setImmediatecallbacks. - Close callbacks — e.g.
socket.on('close', ...).
Microtasks (promises) run between every phase, not just at the end of a full cycle.
setImmediate vs setTimeout(fn, 0)
Both look similar but fire in a different phase:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));At the top level, the order between these two isn't guaranteed — it
depends on process startup timing. But inside an I/O callback,
setImmediate always runs first, because the poll phase leads directly
into the check phase:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// Logs: immediate, timeoutWhy this matters
Blocking the event loop with expensive synchronous work — a huge JSON
parse, a tight CPU-bound loop — freezes every other request Node is
handling, because there's only one thread running JavaScript. For CPU-heavy
work, offload it to a worker_thread or a separate process instead of
running it inline.
Comments 0