Do you really know the difference between an algorithm and a text file?
If your answer is no, you probably wanna start here
This section is a follow-up.
In the previous one, we agreed on a few key ideas:
- CPU-bound – All the heavy lifting happens inside your processor.
- I/O-bound – Your code is waiting on external entities (files, networks, that one slow AWS API).
Knowing the difference between the two is foundational to scaling.
Because how do you scale something like an FFmpeg node worker?
video -> [READ: I/O][PROCESS: CPU][WRITE: I/O] -> output
We used the restaurant analogy:
- Your Computer Is a Kitchen
- Scaling Laws of the Kitchen:
- Real-world CPU-heavy tasks examples:
- Thread-Based Scaling
- Beyond the Basics
Your Computer Is a Kitchen
- Main thread = Head chef
- Cores = Workstations
- Programming language = Knives/utensils
- I/O = Delivery trucks outside
Scaling Laws of the Kitchen:
- More trucks ≠ faster chopping (Adding I/O won’t fix CPU limits)
- Better knives ≠ faster deliveries (Optimizing code won’t fix slow disks, networks, or databases)
Now let’s talk scaling:
- Vertical – Beefing up one machine with more memory and cores
- Horizontal – Distributing work across internal clusters (processes) or separate machines
In this article, we’ll zoom in on one type of vertical scaling: Threads.
Threads 🧵
Threads are useful for CPU-heavy tasks.
Think of it this way: your Node.js program runs on a single thread (the main thread):
-----------op--------op-----------op-----------op------op
The second operation(op) will never run until the first one completes.
Totally fine for trivial computations; computers are fast, after all.
But if the first op is something like fib(50)
... yeah, it’ll be seconds before anything else runs.
// main.js
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
fib(50);
console.log("I am never running :(");
Real-world CPU-heavy tasks examples:
- Data munging and transformation (once data is in memory)
- Data generation
- Machine learning & inference
- Algorithmic computations
- Image & video processing
- Compression and encryption
This is beyond just “read from DB, save to DB.”
This is where you scale the kitchen; hire more sous chefs (threads).
Thread-Based Scaling
From a single thread:
Single thread: [op1][op2][op3]...
To true parallel execution:
Main thread: [light work][messages][light work][responses][light work]...
Worker 1: [HEAVY OP][HEAVY OP]...
Worker 2: [HEAVY OP][HEAVY OP]...
Each Node.js worker thread runs in isolation with its own memory space.
Creating a Thread
Move the fib
function into a new file worker.js
:
// worker.js
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
In main.js
, spawn a new thread:
import { Worker } from 'node:worker_threads';
const fibWorker = new Worker("./worker.js");
console.log("I am never running :(");
But hold.....these threads run in isolation.
We need a way to tell the worker what to do and for the worker to send results back.
Communication Between Threads
Node.js provides a parentPort
for the worker to communicate.
In the main thread, we already have the fibWorker
object, we can post messages and attach event listeners.
// main.js
import { Worker } from 'node:worker_threads';
const fibWorker = new Worker("./worker.js");
fibWorker.on('message', result => console.log(`Fib(50) = ${result}`));
fibWorker.on('error', err => console.error(err));
fibWorker.on('exit', code => console.log(`Worker exited with code ${code}`));
fibWorker.postMessage(50);
console.log("I am never running :(");
In the worker thread:
// worker.js
import { parentPort } from 'node:worker_threads';
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
parentPort.on("message", msg => {
if (Number(msg)) {
parentPort.postMessage(fib(msg)); // Send result back
}
});
Now when you run:
node .\main.js
I am never running :(
Fib(50) = 12586269025 // after a few seconds
Beyond the Basics
We’ve only scratched the surface.
There are other ways to communicate between threads:
- Shared memory (
SharedArrayBuffer
) — most efficient, avoids copying - Broadcasting over channels
- Communication via thread IDs
And there's even more to consider; like low level thread implementation.
If there's one thing you should always know about threads in a language:
Are they OS-level threads or something else?
- OS-level threads are heavy. Use them sparingly.
- Assume threads are OS-level unless explicitly told otherwise.
For example:
In Go, goroutines are not OS threads; so spawning millions is fine.
What We Just Did
- Spawned a thread
- Established communication
- Delegated heavy computation
This was just a small exploration, but in today’s climate, depth matters.
This is my attempt at taking you there.
Node.js isn’t just a CRUD runtime ; it’s a C++ engine under the hood.
If you ever dare to explore the belly, the abyss of Node.js and read its source code...
You’ll never look at it the same way again.
It’ll change how you build.... forever.
You can find me on x.
More Content
Free:
Top comments (1)
Resources
Node.js Workers
Difference between User Level thread and Kernel Level thread