C.W.K.
Stream
Lesson 04 of 05 · published

The Single-Threaded Model (And Its Escape Hatches)

~13 min · runtime, threading, worker-threads, concurrency

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"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

CPU-bound workloads on the main thread are Node's failure mode. Image processing, ML inference, large JSON parsing, password hashing — these compute for tens or hundreds of milliseconds and block the entire event loop while doing so. Your latency p99 explodes. Your healthchecks time out. Other requests pile up behind the one slow operation. The fix is never "make the sync code faster" — it's "get it off the main thread."

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

For a long time I treated "Node is single-threaded" as a slogan I should repeat. Dad made me say what it *means* in practice: "if I do a 500ms CPU thing on the main thread, every other request waits." That sentence is the test. If your service's p99 latency is much worse than your p50, suspect a hidden CPU stall on the main thread. Profile with --inspect and look for synchronous gaps — those gaps are where single-threaded Node failed you and worker_threads would have saved you.

Code

Offloading CPU to a worker_thread·javascript
// main.mjs — offload CPU work to a worker thread
import { Worker } from 'node:worker_threads';

function hashOffMain(payload) {
  return new Promise((resolve, reject) => {
    const w = new Worker(new URL('./hash-worker.mjs', import.meta.url), {
      workerData: payload,
    });
    w.once('message', resolve);
    w.once('error', reject);
    w.once('exit', (code) => {
      if (code !== 0) reject(new Error(`worker exit ${code}`));
    });
  });
}

const sig = await hashOffMain('hello world');
console.log(sig);

// hash-worker.mjs — runs on its own V8 isolate, its own thread
import { parentPort, workerData } from 'node:worker_threads';
import { createHash } from 'node:crypto';

const h = createHash('sha256').update(workerData).digest('hex');
parentPort.postMessage(h);  // main loop unblocked the whole time
What single-threaded blocking looks like in production·javascript
// The wrong way — blocks the main loop for the duration
import { createHash } from 'node:crypto';

app.post('/sign', (req, res) => {
  // If this takes 200ms, ALL other requests wait 200ms
  const sig = expensiveSign(req.body);
  res.json({ sig });
});

// Symptom: p50 latency 20ms, p99 latency 800ms.
// Cause: head-of-line blocking by the slow CPU path.
// Fix: worker_threads, or use crypto.scrypt's async form,
// or precompute, or move the work out of Node entirely.

External links

Exercise

Write a sync function fib(n) that computes the n-th Fibonacci number recursively (deliberately slow). Build a tiny HTTP server with two routes: /fast returns immediately, /slow calls fib(40) then returns. Hit /slow once, then immediately hit /fast. Time /fast. Now do the same after moving fib(40) into a worker_thread. The difference is single-threaded blocking made visible.
Hint
Use curl -w "%{time_total}\n" http://localhost:3000/fast to measure. Without the worker, /fast should be delayed by however long fib(40) took. With the worker, /fast should respond in single-digit milliseconds even while fib(40) is still computing.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.