"Single-threaded JavaScript is a feature, not a limitation — until you do CPU work, and then it's both."
What "Single-Threaded" Actually Means
When people say "Node is single-threaded," they mean exactly one thing: your JavaScript runs on one OS thread. There's a single V8 isolate, a single call stack, a single event loop. No matter how many CPU cores your machine has, your JS uses one of them — unless you explicitly opt into more (Workers, child processes, cluster module).
This is why you never need locks, mutexes, or semaphores in regular Node code. The JavaScript you wrote can't be interrupted mid-statement by another piece of JavaScript. counter++ is atomic from your code's perspective because there's no other thread that could read or write counter at the same moment.
The flip side: any CPU work you do on the main thread blocks everything. While your code spends 200ms computing a JSON Web Token signature or hashing a password, Node can't accept new connections, can't fire timers, can't drain the microtask queue. The whole event loop waits for you to finish.
When Single-Threaded Fits
Node was designed for one workload: I/O-bound servers handling many concurrent connections. Web APIs, proxies, real-time websocket fan-out, file servers, database clients. In these workloads, your code spends most of its time waiting — waiting for the database, waiting for the network, waiting for disk. Single-threaded code with non-blocking I/O wins because you don't pay the cost of context-switching between thousands of OS threads.
A classic comparison: a threaded server (think Java's thread-per-request model) running 10,000 connections needs roughly 10,000 threads, each with a 1MB stack — that's 10GB of memory before you've done any real work. A Node server handles 10,000 connections in one thread, with the connections living as JS objects in a single V8 heap. Memory savings: orders of magnitude.
When Single-Threaded Hurts
The Escape Hatches
Node gives you three ways to actually use more cores:
- worker_threads — spawn a separate V8 isolate on another OS thread. Each worker has its own event loop, its own memory, its own JS context. Communicate via message passing (or SharedArrayBuffer for hot paths). This is the modern answer for CPU work.
- child_process — spawn a separate Node (or other) process. Heavier than workers but stronger isolation. Crash in the child doesn't take down the parent.
- cluster — spin up N copies of your Node app on the same port, OS load-balances connections. Older mechanism, mostly superseded by running N processes behind a reverse proxy or container orchestrator.
Default to worker_threads for CPU work. They're cheaper than child processes and share less than you'd expect (V8 doesn't share heap between workers — that's part of why it's safe).
Pippa's Confession
--inspect and look for synchronous gaps — those gaps are where single-threaded Node failed you and worker_threads would have saved you.