Node.js Event Loop & Architecture

Difficulty: Intermediate

Question

How does the Node.js event loop work? How is it different from the browser event loop?

Answer

Node.js uses a single-threaded event loop powered by libuv for non-blocking I/O. While the main thread is single, I/O operations (file, network, DNS) are offloaded to libuv's thread pool or OS async mechanisms.

Node.js event loop phases: 1. Timers: setTimeout, setInterval callbacks 2. Pending callbacks: deferred I/O callbacks 3. Idle/Prepare: internal use 4. Poll: retrieve new I/O events, execute I/O callbacks 5. Check: setImmediate callbacks 6. Close callbacks: socket.on('close')

Microtasks (process.nextTick, Promise callbacks) run between each phase.

Code examples

Node.js Event Loop Order

console.log('1: sync');

setTimeout(() => console.log('2: setTimeout'), 0);

setImmediate(() => console.log('3: setImmediate'));

process.nextTick(() => console.log('4: nextTick'));

Promise.resolve().then(() => console.log('5: promise'));

console.log('6: sync');

Sync first, then nextTick (highest priority microtask), then Promise microtask, then timer macrotask, then setImmediate (check phase).

Blocking the Event Loop (Anti-Pattern)

const express = require('express');
const app = express();

// BAD: blocks the event loop for ALL users
app.get('/hash', (req, res) => {
  const crypto = require('crypto');
  // This takes ~500ms and blocks everything
  const hash = crypto.pbkdf2Sync(
    'password', 'salt', 100000, 64, 'sha512'
  );
  res.json({ hash: hash.toString('hex') });
});

// GOOD: non-blocking with async version
app.get('/hash-async', async (req, res) => {
  const crypto = require('crypto');
  const hash = await new Promise((resolve, reject) => {
    crypto.pbkdf2(
      'password', 'salt', 100000, 64, 'sha512',
      (err, key) => err ? reject(err) : resolve(key)
    );
  });
  res.json({ hash: hash.toString('hex') });
});

Sync crypto blocks the event loop, making the server unresponsive. The async version offloads to the thread pool.

Worker Threads for CPU-Intensive Work

// main.js
const { Worker } = require('worker_threads');

function runWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', {
      workerData: data,
    });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

// worker.js
const { workerData, parentPort } = require('worker_threads');

// CPU-heavy computation runs in separate thread
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const result = fibonacci(workerData);
parentPort.postMessage(result);

Worker threads run JavaScript in parallel threads - ideal for CPU-intensive work without blocking the main event loop.

Key points

Concepts covered

Event Loop, libuv, Non-blocking I/O, Single Thread, Worker Threads