You learn JavaScript in the browser, you write a few app.get('/users') handlers in Node, and the syntax looks the same. So you tell yourself this is just JavaScript with a different import. That mental model holds for about a week of real traffic.
Then a CPU-heavy handler stalls every other request. A for loop over a 200 MB JSON file pegs the process. A third-party library calls fs.readFileSync deep inside a hot path and your p99 latency triples. None of this is a JavaScript problem. It's a runtime problem, and the runtime has rules the language doesn't tell you about.
This piece is about those rules. Not the trivia version — the version that helps you predict what your service will do at 3 a.m.
Node Is A Runtime, Not A Language
JavaScript is a spec. V8 is an engine. Node.js is a runtime that wraps V8 with a C++ layer called libuv, plus a standard library written for servers: fs, net, http, crypto, worker_threads, child_process, streams, and so on. Bun and Deno are alternative runtimes for the same language.
That distinction matters because every Node-specific behavior you have to debug — the event loop, the worker pool, file descriptors, signals, process exit — lives in the runtime, not in the language. Array.prototype.map works the same everywhere. setImmediate does not exist in the browser at all. When something behaves "weirdly," the cause is almost always the runtime.
A clean test: if you can't reproduce a bug in a browser tab, it's a Node bug, and the JavaScript skill that fixes it is mostly secondary.
Single-Threaded, But Not Single-Worker
The famous line is "Node is single-threaded." That's true for your JavaScript. There is one main thread executing your code, one call stack, one event loop turn at a time. If you block it, you block everything: every request, every timer, every scheduled callback.
But under that thread, libuv runs a worker pool (default size 4) for things that can't be done non-blocking on the OS: file system operations, DNS lookups via getaddrinfo, some crypto work like pbkdf2, and zlib. Those happen off-thread, then their results come back to your code as callbacks. You can resize the pool with UV_THREADPOOL_SIZE.
// Looks innocent. Runs on the main thread. Blocks everything.
app.get('/report', (req, res) => {
const rows = JSON.parse(fs.readFileSync('./big.json', 'utf8'));
res.json(rows.filter(r => r.active));
});
The fix isn't "use async." Async file reads still hit the worker pool, and JSON.parse of a 200 MB string is pure CPU on the main thread no matter how you wrap it. The fix is to stream, paginate, or move the work to worker_threads.
I/O Is Cheap, CPU Is Expensive
The thing Node is genuinely great at is starting many slow I/O operations and waiting for them concurrently. A request that does three database calls and one HTTP fetch barely uses CPU — it spends most of its time parked on epoll/kqueue, waiting for the OS to say "your data is ready."
This is why a single Node process can handle thousands of concurrent connections on cheap hardware. It's also why CPU work is the one thing that breaks the model. Hashing a password with bcrypt, rendering a PDF, parsing a giant CSV, encrypting a payload — these don't yield to the loop, and while they run, every other request waits.
The escape hatches are real but specific: worker_threads for in-process CPU work, child_process for separate processes, the cluster module for forking the whole server, or moving the work entirely to a queue with BullMQ and a separate worker service. These are three different tools for three different problems — don't conflate them.
The Standard Library Is The Real Story
Most "Node is great" arguments are really arguments about the standard library and the C++ glue around it. A few APIs that quietly shape your architecture:
- Streams (
Readable,Writable,Duplex,Transform) withpipeline()for composing them. They give you backpressure for free if you use them correctly. AbortController/AbortSignal— first-class everywhere now. Use them to cancelfetch, timers, file reads, and your own long-running work.fetchbuilt in since Node 18 (experimental) and stable since Node 21. No morenode-fetch.node:test— the native test runner shipped experimentally in Node 18 and went stable in Node 20. You don't actually need Jest for a small service anymore.--watchflag for dev (Node 18+).nodemonis no longer required for most cases.worker_threads/cluster/child_process— three different concurrency tools, in increasing weight. Workers share memory viaSharedArrayBuffer; clusters fork the whole process; child processes are unrelated programs.
Knowing what's already in node: saves you a dependency and usually a class of bugs. Modern Node is much closer to "batteries included" than the 2016 reputation suggests.
What This Means For Code Review
When you read a Node PR, the JavaScript questions ("is this map idiomatic?") are the easy part. The runtime questions are the load-bearing ones:
- Does this block the loop? Sync file I/O, big
JSON.parse, regex with catastrophic backtracking, hashing, largeforloops over arrays of objects. Anything that runs more than a few milliseconds withoutawaitis suspect. - Where does this fail? Network calls, DB queries,
fsoperations, child processes — each fails differently and needs its own error path. - Is cancellation wired up? A request that drops should not keep three downstream calls alive.
AbortControllershould follow the request lifecycle. - Where do file descriptors go? Open streams, connections, child processes — anything you open, you have to close, especially on shutdown.
- What happens on SIGTERM? Production Node should drain the HTTP server (
server.close()), close DB pools, and let in-flight work finish before exiting.
None of that is JavaScript. It's all runtime behavior, and it's what separates a Node service that runs from a Node service that runs well.
A One-Sentence Mental Model
Node.js is one JavaScript thread sitting on top of an OS-aware C++ scheduler — write code that lets the scheduler do its job, and the runtime will keep up with you; write code that blocks the thread, and no amount of clever JavaScript will save you.






