Skip to content

Node.js Architecture — Complete Guide to Event Loop and libuv Internals

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Node.js Architecture. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js architecture combines V8 JavaScript engine with libuv library, enabling non-blocking I/O through an event loop, thread pool, and asynchronous callback system.

What You'll Learn

By the end of this tutorial, you'll understand Node.js internal architecture, how libuv manages I/O operations, the event loop phases, thread pool usage, and how JavaScript runs asynchronously.

Why Architecture Matters

Understanding Node.js internals helps you write performant code, debug bottlenecks, and choose the right APIs for different workloads.

Real-World Use

A high-traffic API server handles thousands of concurrent requests without blocking because libuv delegates file reads and database queries to separate threads while the event loop processes JavaScript.

Node.js Architecture Learning Path

flowchart LR
  A[Node.js Basics] --> B[Architecture]
  B --> C[Event Loop Phases]
  C --> D[Async Patterns]
  D --> E[Streams]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

High-Level Architecture

Node.js uses four key components: V8 parses and executes JavaScript, libuv handles I/O and threading, the event loop coordinates callbacks, and the binding layer connects C++ to JavaScript.

// Node.js architecture visualized as code layers
const architecture = {
  application: "Your JavaScript Code",
  nodeCore: "Node.js Standard Library (JS)",
  bindings: "C++ Bindings (src/node_*.cc)",
  v8: "JavaScript Engine (parse, JIT, GC)",
  libuv: "I/O Engine (event loop, thread pool, filesystem, networking)",
};
console.log("Node.js runs on:", Object.keys(architecture).join(" -> "));
// Node.js runs on: application -> nodeCore -> bindings -> v8 -> libuv

The libuv Library

libuv handles all asynchronous I/O that V8 cannot. It manages the event loop, thread pool (default 4 threads), filesystem operations, DNS lookups, and signal handling.

const os = require("node:os");
// libuv thread pool size is controlled by UV_THREADPOOL_SIZE
console.log("Default libuv thread pool size:", 4);
console.log("CPU cores:", os.cpus().length);
// Recommendation: set UV_THREADPOOL_SIZE = CPU cores * 2 for I/O heavy apps

V8 Engine Integration

V8 compiles JavaScript to machine code, manages memory, and runs Garbage Collection. It provides the call stack where JavaScript executes synchronously.

// V8 call stack in action
function multiply(a, b) {
  return a * b;
}
function square(n) {
  return multiply(n, n);
}
function main() {
  const result = square(5);
  console.log("Result from V8 call stack:", result);
}
main();
// Result from V8 call stack: 25

Thread Pool Operations

libuv maintains a thread pool for operations that the operating system does not provide as non-blocking: filesystem operations, DNS lookup (dns.lookup), CPU-intensive crypto, and compression.

const crypto = require("node:crypto");
const start = Date.now();
// pbkdf2 uses libuv thread pool
for (let i = 0; i < 4; i++) {
  crypto.pbkdf2("password", `salt${i}`, 100000, 64, "sha512", () => {
    console.log(`Thread ${i} finished in ${Date.now() - start}ms`);
  });
}
// With 4 threads, all 4 complete at roughly the same time
// With 5th call, one waits for a thread to free

Common Mistakes

1. Blocking the Event Loop with CPU Work

Synchronous JSON Parsing or image processing blocks all requests. Use worker threads for CPU tasks.

2. Misunderstanding libuv Thread Pool Size

Default is 4 threads. For file-heavy applications, increase with Process.env.UV_THREADPOOL_SIZE = os.cpus().length * 2.

3. Assuming All I/O Is Non-Blocking

Filesystem operations use the thread pool. Too many concurrent file reads exhaust available threads.

4. Ignoring V8 Garbage Collection Pauses

Large object allocations trigger GC pauses. Use object pools or buffers to reduce allocation pressure.

5. Forgetting process.nextTick Runs Between Phases

nextTick callbacks execute before the next event loop phase, potentially starving I/O if called recursively.

Practice Questions

1. What are the four main components of Node.js architecture?

V8 JavaScript engine, libuv I/O library, Node.js core library (JavaScript), and C++ bindings connecting them.

2. What is the default libuv thread pool size?

  1. It can be changed via UV_THREADPOOL_SIZE environment variable.

3. Which operations use the libuv thread pool?

Filesystem operations, crypto (pbkdf2, randomBytes), DNS lookup, and zlib compression.

4. Why does Node.js use libuv instead of just V8?

V8 only handles JavaScript execution. libuv provides cross-platform async I/O, event loop, and thread management.

5. Challenge: Create a benchmark that shows thread pool saturation with 8 concurrent crypto operations.

const crypto = require("node:crypto");
const os = require("node:os");
const start = Date.now();
const total = os.cpus().length * 2;
for (let i = 0; i < total; i++) {
  crypto.pbkdf2("pass", `s${i}`, 100000, 64, "sha512", () => {
    console.log(`Done ${i} at ${Date.now() - start}ms`);
  });
}

FAQ

Is Node.js single-threaded?

JavaScript execution is single-threaded. libuv manages a thread pool for I/O, but your JavaScript code runs on one thread.

What is the difference between V8 and libuv?

V8 compiles and runs JavaScript. libuv handles async I/O, event loop, and threading. Both work together in Node.js.

Can I change the libuv thread pool size?

Yes. Set process.env.UV_THREADPOOL_SIZE before any async operation, or pass it as an environment variable.

Does the event loop run on the main thread?

Yes. The event loop runs on the main thread alongside your JavaScript code, coordinating callbacks.

What happens when all thread pool threads are busy?

Operations queue up and wait for a thread to become available. This can increase latency under load.

Mini Project: Thread Pool Monitor

Build a monitoring script that measures thread pool utilization under different loads.

const crypto = require("node:crypto");
const os = require("node:os");
process.env.UV_THREADPOOL_SIZE = os.cpus().length;
function measureConcurrency(count) {
  const start = Date.now();
  let done = 0;
  for (let i = 0; i < count; i++) {
    crypto.pbkdf2("pass", `s${i}`, 100000, 64, "sha512", () => {
      done++;
      if (done === count) {
        const elapsed = Date.now() - start;
        console.log(`${count} operations: ${elapsed}ms (${(elapsed / count).toFixed(1)}ms per op)`);
      }
    });
  }
}
measureConcurrency(4);
measureConcurrency(8);
measureConcurrency(16);

What's Next

Node.js Event Loop Phases Node.js Async Patterns Node.js Streams

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro