Skip to content

Why the Runtime Model Matters

Every earlier module in this course taught you to use Node: write a promise, read a file, open a socket, publish a package. That is enough to write correct Node code by hand. It is not enough to review Node code you did not write — and today, a growing share of the Node in front of you was generated by a tool that reproduces common shapes, not a person reasoning about the threadpool, the heap, or the module loader while typing.

AI-generated Node code runs for one of two very different reasons: because it is actually correct, or because the specific input it was tested against never triggered the runtime behavior that would break it. Promise.all over 500 items that “worked in the demo,” a require() that happens not to hit a cycle, a deep-merge that never receives a __proto__ key — all of these run with zero errors in the common case, and all of them look, to someone who only knows Node’s vocabulary, exactly like correct code. Telling them apart requires a model of what actually executes underneath the JavaScript, not just familiarity with the APIs.

Every Node process is the same stack, top to bottom:

flowchart TB
JS["Your JavaScript
functions, promises, closures"] --> V8["V8
parses, JITs, and runs JS; owns the heap and the garbage collector"]
V8 --> Libuv["libuv
the event loop, the threadpool, and OS-level async I/O bindings"]
Libuv --> OS["Operating system
files, sockets, processes, signals"]
What a Node process is actually made of

V8 is the JavaScript engine — it is also where the heap lives, which means every object you allocate and every “memory leak” you chase is a V8 concept, not a Node one. libuv is the C library that gives Node its event loop, its small fixed-size threadpool, and the glue to the OS’s native async I/O (epoll, kqueue, IOCP) — this is what makes “non-blocking” true instead of aspirational. Node itself is the layer that wires the two together and exposes them as fs, http, worker_threads, and the rest of the standard library you already know.

This module’s five lessons walk that stack from the inside out: V8 & Memory covers the heap and garbage collection — where leaks actually live and how to measure them. libuv & the Event Loop goes deeper than the event-loop module on phase internals, timer ordering, and what blocking really costs. Module Loading is about the mechanism CommonJS and ESM use to turn a file into a running module, including where they disagree. Native Code & Workers is about work that leaves the single JS thread entirely — worker threads, shared memory, and child processes. The Security Model covers the boundaries Node gives you (and the ones it explicitly does not) between your code and the rest of the machine.

process.versions is not a curiosity — it is the version of every one of these layers baked into the binary you are running, and it makes the four-layer picture above concrete instead of conceptual:

Node.js
Output (verified on Node 24)
Node.js itself: v24.13.0
V8 engine (the JS layer): 13.6.233.17-node.37
libuv (the event loop + threadpool layer): 1.51.0

Node ships one specific V8 and one specific libuv per release — this is exactly why a claim like “strip-types is default in Node 24” or “the threadpool is 4 threads” is a fact about a version, not about “Node” in the abstract, and why this module states which Node version every verified number below came from.

Think of a restaurant. The dining room is V8 — this is where your JavaScript actually executes, one order (one call stack) at a time, on a single waiter (the main thread) who can only carry one thing at once. The kitchen is libuv: a small back room with four cooks (the default threadpool) for the handful of dishes that must be made in-house (certain fs, crypto, dns.lookup, zlib calls), plus a direct phone line to outside vendors (the OS kernel’s native async I/O) for everything else — sockets, timers — that does not need a cook at all. The building itself is the OS: the gas line, the electricity, the actual walls that both the dining room and kitchen depend on and cannot see around.

Understanding this stack has a cost: it is one more mental model to hold, on top of the JavaScript language itself, before you can confidently read a Node file. The payoff is that the cost is paid once and amortizes over every file you will ever review — “why is this Promise.all slow,” “why did this leak,” “why does this behave differently in a worker” all reduce to which layer is responsible, instead of needing a fresh guess each time.

  • “Node is single-threaded, full stop.” Your JavaScript runs on a single thread. The process is not single-threaded — libuv’s threadpool and the OS’s own I/O handling both run outside that one JS thread, which is exactly why they do not block it.
  • “The event loop and the threadpool are the same thing.” The event loop is libuv’s main loop, running on the same thread as your JS. The threadpool is a separate, fixed-size set of worker threads the loop can hand specific work to. Confusing the two leads to wrong claims about what “async” can and cannot parallelize (see libuv & the Event Loop).
  • “Memory leaks are a Node-specific bug category.” A “leak” in a Node program is a V8 heap-retention problem — the same category of bug as in any garbage-collected language, just reachable through Node-specific patterns like a growing closure or an unbounded EventEmitter listener list (see V8 & Memory).

A generated snippet that “just works” in a quick test is not evidence it is safe under load, under a hostile input, or across a version bump — all three of those depend on the runtime layers above, not on the JavaScript syntax alone.

🐛 Spot the bug

// AI-written helper, added to a request handler
async function hashAllPasswords(candidates) {
// "async, so it runs all of these in parallel" -- comment from the AI
return Promise.all(candidates.map((c) => hashPassword(c)));
}
// hashPassword() uses crypto.pbkdf2, which runs on libuv's threadpool
Show the answer

This looks like it “parallelizes” the work because it is wrapped in Promise.all, and it will pass any test with a handful of candidates. But crypto.pbkdf2 runs on libuv’s fixed threadpool (4 threads by default) — under a real request with hundreds of candidates, only 4 run at true hardware parallelism at a time, and because the pool is shared process-wide, this call can also slow down unrelated fs/dns.lookup/zlib work happening elsewhere in the same process. The bug is not in the JavaScript; it is in not knowing which layer pbkdf2 actually runs on. V8 & Memory and libuv & the Event Loop cover exactly this kind of “correct-looking JS, wrong runtime assumption.”

Which layer of the Node stack owns the heap and the garbage collector?
What does this print?
console.log(typeof process.versions.uv, typeof process.versions.v8);
Why is "Node is single-threaded" an oversimplification?
What is wrong with the AI-written comment on this line? hashPassword() calls crypto.pbkdf2 internally, which runs on libuv's threadpool.
async function hashAllPasswords(candidates) {
  // "async, so it runs all of these in parallel" -- comment from the AI
  return Promise.all(candidates.map((c) => hashPassword(c)));
}
Which of the following is true about libuv?