Processes vs Threads in Node.js and Bun
When scaling JavaScript execution, developers must choose between three core approaches: single-threaded loop, child processes , and isolated worker threads. This post analyzes the performance of those three approaches using Node.js and Bun:
- cpu-math: floating point loop.
- cpu-crypto: hashings via OpenSSL
- transferable: 100MB ArrayBuffer transfer.
Core architecture
A common flaw is that running benchmarks measures the allocation time of a resource rather than the execution performance.
In production systems, workers are typically long-lived resources. For this benchmark, worker threads and child processes are created once and reused through pools.
const pool = new WorkerPool(
new URL('./worker-task.js', import.meta.url),
THREADS
);
// Warmup
await Promise.all(
Array.from({ length: THREADS }, () => pool.runTask({ workloadType }))
);
Workload suite:
Floating point calculation
This workload stays within JavaScript, and the runtime spends nearly all of its time executing generated machine code.
function cpuMath() {
let result = 0;
for (let i = 0; i < 5_000_000; i++) {
result += Math.sqrt(i) * Math.sin(i);
}
return result;
}
Cryptographic hashing
This workload lands inside OpenSSL.
function cpuCrypto() {
let result = '';
for (let i = 0; i < 1_000_000; i++) {
result = crypto
.createHash('sha512')
.update(`${i}-{result}`)
.digest('hex');
}
return result;
}
Binary payloads
This workload measures memory movement rather than computation.
function transferablePayload() {
return new ArrayBuffer(100 * 1024 * 1024); // 100MB allocation
}
Profiler:
The profiler isolates parent CPU usage from child CPU execution times. It also reports child memory distribution for three dimensions: average, peak and combined real pressure on the OS.
start() {
startCpu = process.cpuUsage();
startTime = performance.now();
childMetrics = []; // Idempotent cleanup to prevent cross-run leaks
},
recordChild(pid, cpuMicros, rssBytes) {
childMetrics.push({ pid, cpuMicros, rss: rssBytes });
},
stop() {
const endTime = performance.now();
const endCpu = process.cpuUsage(startCpu);
const elapsedMs = endTime - startTime;
// Parent orchestrator footprint metrics
const parentCpuMicros = endCpu.user + endCpu.system;
const parentCpuPercent = (parentCpuMicros / (elapsedMs * 1000)) * 100;
const parentMem = process.memoryUsage();
// Child processes footprint metrics
const childCount = childMetrics.length;
let sumChildCpuMicros = 0;
let sumChildRssBytes = 0;
let maxChildRssBytes = 0;
for (const cm of childMetrics) {
sumChildCpuMicros += cm.cpuMicros;
sumChildRssBytes += cm.rss;
if (cm.rss > maxChildRssBytes) {
maxChildRssBytes = cm.rss;
}
}
}
Findings:
#1: Threads and Processes scale almost identically for pure compute
The gap between worker threads and child processes is small enough for raw computation. This exposes a misconception that threads are lighter than processes.
Runtime cpu-math
======= =======
Worker Threads 0.41s
Child Processes 0.44s
Once all cores are busy with floating-point work, both models spend most of their time executing instructions rather than communicating. The benchmark suggests that once work becomes enough CPU-bound, the distinction between threads and processes becomes far less important. The OS system scheduler becomes the dominant factor here once the core is already saturated.
#2: Process isolation outperformed worker isolation for crypto
Worker threads required almost four times as long to complete the same workload.
Runtime cpu-crypto
======= =======
Worker Threads 7.80s
Child Processes 2.04s
This is because worker threads share the same Node.js process. While they run in separate V8 isolates and heaps, they still contend for shared process-level resources, causing lock contention (under heavy parallel workloads).
Child processes do not do this. They receive their own isolated runtime instance.
For heavy native workloads, process isolation is more effective than thread isolation on this machine. This is important because many production services spend more time in native libraries (OpenSSL, database drivers etc.) than inside JavaScript.
#3: Zero copy =/= Zero Cost
Each worker allocated a 100 MB ArrayBuffer before ownership could be transferred. With eight workers running concurrently, the runtime was managing roughly 800 MB of newly allocated memory before accounting for overhead, page tables, and process bookkeeping.
Runtime transferable
======= =======
Single Thread 0.04s
Child Processes 0.07s
Worker Threads 0.21s
Worker threads peaked at ~2GB RSS during execution. The important learning is that transferables eliminate copy costs, but not the allocation cost. Zero-copy doesn't mean zero-cost.
#4: Runtime selection is more important than concurrency selection
The largest performance gap in the entire benchmark was not: worker_threads vs child_process
It was Node.js vs Bun. Bun completed the floating-point workload almost six times faster than the equivalent Node.js worker implementation.
For the floating point calculation:
Runtime cpu-math
======= =======
Worker Threads 0.41s
Child Processes 0.44s
Bun Workers 0.07s
Node.js executes on V8 while Bun executes on JavaScriptCore. V8 prioritizes aggressive optimization (speculative) and deoptimization strategies through a multi-stage pipeline. JavaScriptCore follows a different optimization strategy and has historically performed well on predictable numeric workloads. While the benchmark does not prove why JavaScriptCore won, the gap between Bun and Node.js was larger than the gap between worker threads and child processes.
Closing thoughts:
The common discussion in Node.js is usually "worker threads vs child processes." This benchmark shows that the question is often secondary.
For pure computation, worker threads and child processes scaled almost identically. For native crypto workloads, child processes significantly outperformed worker threads. For large binary payloads, transferables removed copy costs but introduced substantial memory pressure.
The first question should not be "threads or processes?" The first question should be "What is the workload actually doing?"