DEV Community

Cover image for Vertical Scaling with Node.js: Threads.
Sk
Sk

Posted on

Vertical Scaling with Node.js: Threads.

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
Enter fullscreen mode Exit fullscreen mode

We used the restaurant analogy:


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:

  1. More trucks ≠ faster chopping (Adding I/O won’t fix CPU limits)
  2. 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
Enter fullscreen mode Exit fullscreen mode

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 :(");
Enter fullscreen mode Exit fullscreen mode

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]...
Enter fullscreen mode Exit fullscreen mode

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]...
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 :(");
Enter fullscreen mode Exit fullscreen mode

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 :(");
Enter fullscreen mode Exit fullscreen mode

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
  }
});
Enter fullscreen mode Exit fullscreen mode

Now when you run:

node .\main.js
I am never running :(
Fib(50) = 12586269025 // after a few seconds
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
sfundomhlungu profile image
Sk