Topic 11 of 55
Worker Threads & CPU Tasks
Overview
Node.js is single-threaded — CPU-intensive tasks (image processing, encryption, data crunching) block the event loop and freeze all requests. Worker Threads run in parallel JavaScript threads, solving this without forking a process.
Syntax
javascript
// main.js
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
import path from 'path';
if (isMainThread) {
// Main thread — offload CPU work to worker
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker(import.meta.url, { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
});
});
}
// Express route — won't block
app.post('/api/generate-report', async (req, res) => {
const result = await runWorker({ orders: req.body.orders });
res.json(result);
});
} else {
// Worker thread — runs in separate thread
const { orders } = workerData;
// Heavy CPU computation — doesn't block main thread!
const report = generateComplexReport(orders);
parentPort.postMessage(report);
}Common Pitfalls
- Worker Threads share no memory by default — communication happens via messages (postMessage/on('message')). Use SharedArrayBuffer for shared memory.
- Creating a new Worker for each request is expensive — use a pool of pre-created workers for high-throughput scenarios.
- Interview tip: Worker Threads vs child_process.fork() — Workers share memory and are lighter; child_process is a full separate Node.js process (more isolation, more overhead).
Real-World Example
A worker pool for image processing:
example
javascript
// imageProcessor.worker.ts
import { parentPort, workerData } from 'worker_threads';
import sharp from 'sharp';
const { imagePath, outputDir } = workerData;
// CPU-intensive image processing
const results = await Promise.all([
sharp(imagePath).resize(1200, 630).webp({ quality: 85 }).toFile(`${outputDir}/og.webp`),
sharp(imagePath).resize(400, 400).webp({ quality: 80 }).toFile(`${outputDir}/thumb.webp`),
sharp(imagePath).resize(100, 100).webp({ quality: 70 }).toFile(`${outputDir}/tiny.webp`),
]);
parentPort!.postMessage({ success: true, files: results });
// imageService.ts — Worker Pool Pattern
import { Worker } from 'worker_threads';
import os from 'os';
class ImageWorkerPool {
private workers: Worker[] = [];
private queue: Array<{ task: any; resolve: Function; reject: Function }> = [];
private idle: Worker[] = [];
constructor(size = os.cpus().length) {
for (let i = 0; i < size; i++) {
const worker = new Worker('./imageProcessor.worker.ts');
worker.on('message', (result) => {
const next = this.queue.shift();
if (next) {
worker.postMessage(next.task);
} else {
this.idle.push(worker);
}
});
this.idle.push(worker);
}
}
process(imagePath: string, outputDir: string): Promise<any> {
return new Promise((resolve, reject) => {
const worker = this.idle.pop();
if (worker) {
worker.postMessage({ imagePath, outputDir });
worker.once('message', resolve);
} else {
this.queue.push({ task: { imagePath, outputDir }, resolve, reject });
}
});
}
}