Event Loop
The Node.js event loop allows Node.js to perform non-blocking I/O operations despite JavaScript running on a single thread by offloading operations to the system kernel whenever possible.
What is the Event Loop?
At its foundation, Node.js uses Google's V8 engine to parse and execute JavaScript synchronously on a single primary call stack. When a script runs, execution units are pushed onto the call stack and evaluated top-to-bottom. However, because network requests, disk reads, and system timers can incur significant latency, running them synchronously would freeze the entire server process.
To solve this without exposing multi-threaded user-space race conditions, Node.js embeds libuv — a high-performance C library that orchestrates asynchronous tasks with native OS asynchronous event demultiplexers (epoll on Linux, kqueue on macOS, and IOCP on Windows).
The event loop is essentially a persistent C++ while loop inside libuv that repeatedly sweeps through dedicated phases. Whenever a non-blocking asynchronous operation finishes, the operating system or worker thread signals libuv, and the corresponding callback is placed into the appropriate phase queue for dispatch back to the JavaScript V8 call stack.
Why does Node.js need it?
Traditional server architectures (such as Apache HTTP Server or early Java enterprise models) rely on allocating an isolated operating system thread for each incoming connection:
Thread-per-Request
Each incoming client connection spawns or consumes an OS thread. Each idle thread reserves 1MB–2MB of stack memory. Context switching overhead scales steeply with concurrent connections.
Single Threaded Non-Blocking
One single event loop thread handles millions of sockets by registering socket handles directly with kernel demultiplexers. Threads are only dispatched for file I/O or crypto calculations.
How it works (Execution Lifecycle)
Each rotation of the event loop is called a tick. Microtasks drain completely between every single callback.
Code Example: Execution Priority Order
The following script demonstrates the interplay between the synchronous call stack, microtask queues (process.nextTick and promises), and macrotask phases (setTimeout and setImmediate):
console.log('1: Synchronous start');
setTimeout(() => {
console.log('2: Timers phase callback (setTimeout 0ms)');
}, 0);
setImmediate(() => {
console.log('3: Check phase callback (setImmediate)');
});
Promise.resolve().then(() => {
console.log('4: Microtask queue (Promise.then)');
});
process.nextTick(() => {
console.log('5: Microtask queue (nextTick - highest priority)');
});
console.log('6: Synchronous end');How to Explain it in an Interview
Recursively scheduling process.nextTick() prevents the event loop from ever entering Phase 1 (Timers), starving all I/O callbacks completely.
By default, libuv allocates 4 background threads. In disk- or crypto-heavy workloads, increase this up to 128 via environment variable before boot.
Complex JSON parsing or regex evaluation locks the main thread, causing severe Event Loop Lag that drops HTTP throughput across all connected users.
What the Interviewer is Testing
Confirming you understand why Node doesn't require explicit lock, mutex, or race-condition primitives in userland.
Knowing when Promise chains execute relative to setImmediate, setTimeout, and socket I/O events.
Awareness of which operations stay on the main thread and which operations are dispatched to background threads.
Ability to pinpoint causes of Event Loop Lag metrics spiking in Datadog or Prometheus under load.
Candidates frequently state that setTimeout(fn, 0) executes instantly in 0 milliseconds. In reality, Node.js clamps all timeouts to a minimum of 1 millisecond (per HTML5 / V8 timer specifications). Furthermore, the timer callback will never execute until:
- The currently executing synchronous call stack completely clears.
- All pending microtasks in both
nextTickQueueand promise microtask queues are exhausted. - The event loop cycles back into Phase 1 (Timers) after the poll phase.