NodeJSNode.js Architecture Overview

Node.js Architecture Overview

We touched on the pieces in How Node.js Works; now let's assemble the complete architectural picture you will carry through the rest of the curriculum. Knowing where each layer sits — and how a request travels through them — explains why certain APIs are asynchronous, why blocking is fatal, where memory and performance problems hide, and how to scale beyond one core. This is the map you will mentally consult every time you debug a Node app.

The layered stack

From your code down to the kernel

Text
┌───────────────────────────────────────────────┐
│  Your application code        (JavaScript)      │
├───────────────────────────────────────────────┤
│  Node.js core API             (JavaScript)      │  fs, http, crypto,
│                                                 │  stream, events, ...
├───────────────────────────────────────────────┤
│  Node.js bindings             (C++)             │  bridges JS ⇄ native
├──────────────────────────┬────────────────────┤
│  V8                       │  libuv              │  (C++ / C)
│  • runs your JavaScript   │  • event loop       │
│  • compiles JS→machine    │  • thread pool      │
│  • garbage collection     │  • async file/net   │
├──────────────────────────┴────────────────────┤
│  Operating system  (syscalls: sockets, files)   │
└───────────────────────────────────────────────┘

Layer

Language

Job

Application code

JavaScript

The program you write

Core API

JavaScript

fs, http, crypto, stream, events, … exposed to you

Bindings

C++

Translate JS calls into native V8/libuv calls

V8

C++

Execute JS, manage the heap and GC — see V8

libuv

C

Event loop, thread pool, async I/O — see libuv

OS

Actual files, sockets, timers, signals

How a request flows through every layer

Trace a single fs.readFile call to watch all the layers cooperate. This exact pattern repeats millions of times in a running server:

  • 1. Your JS calls fs.readFile(path, cb).

  • 2. The core fs module forwards the call to the C++ binding.

  • 3. libuv dispatches the read to its thread pool (files have no good OS-level async API).

  • 4. Your single main thread continues — it does not wait.

  • 5. When the read completes, libuv places your callback on a queue.

  • 6. The event loop picks it up and runs cb(err, data) back inside V8, on the main thread.

Two destinations, one rule
Network I/O goes to the **OS kernel** (epoll/kqueue/IOCP); files, DNS, and crypto go to the **thread pool**. Either way, the result returns as a queued callback that the event loop runs *on your single main thread*. So the rule is universal: whatever runs in that callback must be fast.
The event loop is the conductor

At the centre sits the event loop: a loop that repeatedly asks "is there a callback ready to run?" and runs it. Everything asynchronous — timers, I/O completions, promise reactions, setImmediate — flows back through it. Because there is exactly one loop on one thread, the rule "don't block the loop" governs all Node performance. The loop runs in phases:

One turn of the event loop (simplified)

Text
   ┌──► timers          (setTimeout / setInterval callbacks)
   │    pending I/O      (deferred system callbacks)
   │    poll             (retrieve new I/O; run most callbacks)
   │    check            (setImmediate callbacks)
   └──  close            (e.g. socket 'close' events)

  Between every phase: drain process.nextTick queue, then microtasks (Promises)

We dissect each phase in The Event Loop in Depth and the nextTick/microtask priority in setImmediate & process.nextTick.

Where things actually run

Main thread vs delegated work

JS
// Runs on the MAIN thread, synchronously (blocks while it runs):
const total = items.reduce((s, x) => s + x.price, 0)

// Delegated; the callback runs back on the main thread later:
fs.readFile('data.csv', cb)        // → libuv thread pool
server.on('request', handler)      // → OS kernel network events
crypto.pbkdf2(/* ... */, cb)       // → libuv thread pool (CPU)
setTimeout(fn, 1000)               // → event loop timers phase
Promise.resolve().then(fn)         // → microtask queue
Memory architecture at a glance

V8 manages a small call stack (function frames, primitives) and a larger heap (objects, closures, strings). A separate region, tracked as external memory, holds C++-backed objects like Buffers. You can watch all of it:

JS
const m = process.memoryUsage()
console.log({
  rss: (m.rss / 1e6).toFixed(1) + ' MB',        // whole process
  heapUsed: (m.heapUsed / 1e6).toFixed(1) + ' MB', // live JS objects
  external: (m.external / 1e6).toFixed(1) + ' MB', // Buffers etc.
})
{ rss: '39.7 MB', heapUsed: '5.4 MB', external: '1.2 MB' }
Scaling beyond one core

A single Node process uses one core for JavaScript. To use a whole machine you run multiple processes, and for CPU work inside a process you use threads:

Technique

What it gives you

When to use

cluster module

Fork N processes sharing a listening port

Use all cores for an HTTP server

Worker Threads

Threads sharing memory within one process

CPU-bound work that is part of the app

Child processes

Separate processes (any language)

Run external programs / isolate crashes

Container replicas

Many process instances behind a load balancer

Horizontal scaling across machines

More threads will not speed up I/O
Adding Worker Threads or processes helps **CPU-bound** work. It does *not* make I/O faster — the event loop already handles thousands of concurrent I/O operations efficiently. Reach for threads only when the CPU, not the network/disk, is the bottleneck.
Mental model recap
  • Your code → core API → C++ bindings → V8 (run JS) + libuv (async I/O) → OS.

  • The event loop runs all callbacks on one thread; keep them short.

  • Network uses the kernel; files/DNS/crypto use the thread pool.

  • V8 manages stack + heap; watch heapUsed and external for leaks.

  • Scale CPU with cluster / workers / processes — never by blocking the loop.

Next
Start exploring the runtime's building blocks: [The global Object](/nodejs/global-object).