Skip to main content

Node.js Event Loop & Core Concepts

This guide provides a foundational understanding of how Node.js works, its architecture, and how it handles asynchronous operations.

1. Introduction to Node.js

Node.js is a JavaScript Runtime Environment that allows JavaScript to run outside the browser. It is built by wrapping the Google V8 engine and the Libuv library to enable server-side development. Core Principle:
Node.js is a SINGLE-THREADED environment While JavaScript execution happens on a single thread, Node.js offloads complex tasks to the operating system or its internal thread pool.
Primary Use Cases:
  • I/O Operations: Reading/Writing files, Database calls.
  • Real-time Applications: Chat apps, Game servers.
  • REST APIs: Fast, scalable web services.

2. Event Loop Architecture

The Event Loop is the “heart” of Node.js. It monitors for pending tasks and manages asynchronous operations, enabling non-blocking I/O.

How Node.js Reads Code

  1. Top-to-bottom: Executes synchronous code immediately.
  2. Delegation: Asynchronous tasks (like timers or network calls) are sent to the Event Loop.
  3. Callback Queue: Once a task is done, its callback is queued and processed when the main thread is free.

3. Blocking vs Non-Blocking

Blocking: The thread is “locked” until the operation completes. No other code can run.
const fs = require('fs');

// BLOCKING - Thread waits for file to be read
const data = fs.readFileSync('test.txt', 'utf-8');
console.log(data);
console.log('This waits for file read to complete');
Non-Blocking: The operation is delegated. The thread continues working on other things.
const fs = require('fs');

// NON-BLOCKING - Thread continues execution
fs.readFile('test.txt', 'utf-8', (err, data) => {
  if (err) throw err;
  console.log(data); // Executes when ready
});
console.log('This executes immediately');

4. Event Loop Phases & Priority

The Event Loop executes tasks in a specific hierarchical order:
PhasePriorityTasks
Microtask QueueHighestprocess.nextTick(), Promise.resolve()
Timers1st MacrotasksetTimeout(), setInterval()
I/O Callbacks2nd MacrotaskNetwork / File System results
Poll3rd MacrotaskNew I/O events
Check4th MacrotasksetImmediate()
Close5th MacrotaskSocket closures, cleanup
Output Prediction Example:
console.log('Start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('End');

// Output: Start -> End -> nextTick -> Promise -> setTimeout

5. CPU Intensive Tasks & Multi-Threading

Because of its single-threaded nature, CPU-intensive tasks (like heavy loops) can block the entire server, making it unresponsive to other requests.

Solution 1: Child Processes (fork)

Creates a separate instance of Node.js with its own memory. Good for isolated scripts.

Solution 2: Worker Threads

Threads that share memory with the main process. More lightweight and efficient for mathematical calculations.

Thread Pool

Node.js uses Libuv to maintain a thread pool (default size: 4) for operations like file I/O, hash generation (crypto), and compression (zlib).

6. Streams & Buffers

Processing large data efficiently.
FeatureStreamBuffer
MechanismProcesses data in chunksLoads everything into memory
MemoryEfficient (low footprint)High (can crash on large files)
Use CaseVideo, big logs, file uploadsSmall, fixed-size data
Example (Piping Streams):
const fs = require('fs');
const readStream = fs.createReadStream('source.mp4');
const writeStream = fs.createWriteStream('dest.mp4');

// Transfers data chunk by chunk automatically
readStream.pipe(writeStream);

7. Rate Limiting

Crucial for protecting your server from DDoS attacks and Resource Exhaustion. Implementation: Use libraries like express-rate-limit to restrict requests per IP address.
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 mins
  max: 100 // Limit each IP to 100 requests
});

8. Event-Driven Architecture

Using the EventEmitter to create decoupled, scalable applications.
const EventEmitter = require('events');
const emitter = new EventEmitter();

// 1. Register listener
emitter.on('paymentSuccess', (data) => {
  console.log(`Sending email to ${data.email}...`);
});

// 2. Emit event
emitter.emit('paymentSuccess', { email: 'user@example.com' });

9. Common Interview Questions

Answer:
  • process.nextTick() is a Microtask. It runs immediately after the current operation and before any other phase of the event loop.
  • setImmediate() is a Macrotask. It runs in the “Check” phase of the event loop. Warning: Excessive nextTick can cause “starvation” of the event loop.
Answer: Node.js uses Libuv to manage a pool of background threads. While the event loop handles networking, the thread pool is used for:
  • File System operations (fs)
  • Cryptography (crypto)
  • DNS lookups
  • Compression (zlib)
Answer:
  • fork(): Specialized for Node.js. Creates a new Node engine instance and an IPC channel for easy communication.
  • spawn(): General purpose. Used to run any system command (like python or git) as a separate process.
Answer: Since it’s single-threaded, a single heavy computation (e.g., image resizing) will block the thread, making the server unresponsive for all other users. For such tasks, it’s better to use Worker Threads or offload the work to a specialized service (like a Python microservice).