Node.js95 min total · 14 parts
Node.js Fundamentals: The Runtime, the Event Loop, and Building Real APIs
Part 2 of 14 · ~4 min
What Node.js Actually Is
Start with the question support's ticket actually raises: how does one Node process handle thousands of webhook deliveries arriving from Meridian at once, well enough that "one process, one thread" sounds like it shouldn't work at all — and yet also stall completely for eleven seconds on a single request? Both things are true, and they're true for the same underlying reason.
Node is two separate pieces of engineering duct-taped together, plus a layer of JavaScript-facing APIs (fs, http, process, and the rest) built on top of both:
- V8 — Chrome's own JavaScript engine, embedded rather than borrowed. It parses, compiles, and runs your code, and owns the heap it allocates into, garbage collection included. Ask V8 to open a file or a socket and it has nothing to offer — outside of executing JavaScript, it does nothing at all.
- libuv — the C library doing the part V8 was never built for: driving the event loop, farming out a handful of background threads for work that can't be made properly async, and wrapping the OS's own file, network, DNS, and timer primitives in a form Node can call into. Without libuv, "run this JavaScript" is all Node could ever promise; with it, one process can juggle a Meridian retry storm across thousands of sockets at once.
billhook's route handlers
│
▼
Node.js APIs (fs, http, crypto, process, ...)
│
┌────┴─────┐
▼ ▼
V8 libuv
(runs JS) (event loop, thread pool, async I/O, timers)
The detail worth sitting with: your JavaScript runs on exactly one thread, the same as a script tag in a browser tab. One call stack. V8 executes one statement, then the next, never two at once — Node being "a server thing" buys you nothing here. What Node adds is a second, independent system, living entirely in libuv, whose whole job is absorbing the waiting part of I/O so the JS thread is free the whole time.
Here's what that looks like on billhook's very first job: writing an incoming receipt image to disk after Meridian hands it over.
const fs = require("fs");
console.log("1: starting the write");
fs.writeFile("./receipts/ord_4471.pdf", receiptBuffer, () => {
console.log("3: receipt saved");
});
console.log("2: next webhook can be accepted right here");
// Output: 1, 2, 3 — the disk write happens off the JS thread; the callback
// only runs once the JS thread is free again and the event loop gets to it
What actually happens on that fs.writeFile call: the JS thread doesn't sit there waiting. It drops the write request off with libuv and moves on to the next line immediately. Most filesystem work has no OS-level async equivalent to hook into, so libuv quietly farms it out to a background thread pool — four worker threads, unless you configure it otherwise — while network sockets on Linux and macOS get a cheaper path through the kernel's own async notification APIs and skip the pool entirely. Either route, the JS thread never blocks on it: once the write actually lands, libuv drops a finished callback into the right queue, sitting there until the loop cycles around to give it a turn.
Put the whole architecture in one line: your code owns a single thread, and everything else is a background system tapping that thread on the shoulder when something's ready for it. Which is also the reason billhook barely notices holding a few thousand Meridian connections open and idle at once — each one is just a socket sitting in the OS's bookkeeping, not a thread billhook is paying for. And it's the reason the eleven-second freeze was never an I/O story at all: I/O was never what stalled. Something held that single thread hostage for eleven whole seconds without ever handing it back — what, exactly, is chapter six's problem to solve.
Common mistake: hearing "single-threaded" and assuming Node must be bad at handling lots of things simultaneously. For I/O, it's the opposite — that's the entire architecture above, working as intended. The actual limit is narrower and easy to lose track of: your own code, specifically, never executes two statements in parallel, no matter how many requests are in flight. Miss that distinction and you'll either avoid Node for jobs it's genuinely good at, or get blindsided later when one unusually slow, CPU-heavy line quietly holds every other request hostage behind it.