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
┌───────────────────────────────────────────────┐ │ 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 |
|
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
fsmodule 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.
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)
┌──► 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
// 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 queueMemory 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:
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 |
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
heapUsedandexternalfor leaks.Scale CPU with cluster / workers / processes — never by blocking the loop.