Node.js is single-threaded by default, making CPU-intensive tasks a performance bottleneck. NestJS leverages worker threads to solve this—offloading heavy operations without blocking the main event loop.
You’ll quickly find that your app freezes, blocks requests, or slows down. That’s because Node.js is single-threaded by default.
So the challenge is:
How do we handle CPU-heavy workloads without blocking the main thread?
Let’s dive deep into how to implement true parallelism in Node.js using multi-threading techniques and learn which is best for your use case.
Why Worker Threads?
- Problem: Synchronous CPU-heavy tasks (calculations, image processing) block the main thread.
- Solution: Worker threads run parallel to the main thread, freeing the event loop.
- Key Use Cases:
- Mathematical computations (e.g., Fibonacci)
- Image/video processing
- Large dataset parsing
Setup & Dependencies
First, install Node.js (≥ v12) and create a NestJS project:
npm i -g @nestjs/cli
nest new worker-demo
Install sharp for image processing (our example):
npm install sharp
Offload image resizing using sharp.
Worker Setup
src/workers/image.worker.ts
import { parentPort, workerData } from 'worker_threads';
import sharp from 'sharp';
async function processImage(imagePath: string): Promise {
return sharp(imagePath).resize(300, 300).jpeg().toBuffer();
}
processImage(workerData.path)
.then((processed) => parentPort?.postMessage(processed))
.catch((err) => parentPort?.postMessage({ error: err.message }));
import { parentPort, workerData } from 'worker_threads';
import sharp from 'sharp';
async function processImage(imagePath: string): Promise {
return sharp(imagePath).resize(300, 300).jpeg().toBuffer();
}
processImage(workerData.path)
.then((processed) => parentPort?.postMessage(processed))
.catch((err) => parentPort?.postMessage({ error: err.message }));
Service & Controller
src/image/image.service.ts
import { Worker } from 'worker_threads';
@Injectable()
export class ImageService {
async resizeImage(imagePath: string): Promise {
return new Promise((resolve, reject) => {
const worker = new Worker('./dist/workers/image.worker.js', {
workerData: { path: imagePath },
});
worker.on('message', (message) => {
if (message.error) reject(message.error);
else resolve(message);
});
worker.on('error', reject);
worker.on('exit', (code) => code !== 0 && reject());
});
}
}
import { Worker } from 'worker_threads';
@Injectable()
export class ImageService {
async resizeImage(imagePath: string): Promise {
return new Promise((resolve, reject) => {
const worker = new Worker('./dist/workers/image.worker.js', {
workerData: { path: imagePath },
});
worker.on('message', (message) => {
if (message.error) reject(message.error);
else resolve(message);
});
worker.on('error', reject);
worker.on('exit', (code) => code !== 0 && reject());
});
}
}
src/image/image.controller.ts
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('image')
export class ImageController {
constructor(private readonly imageService: ImageService) {}
@Post('resize')
@UseInterceptors(FileInterceptor('file'))
async resize(@UploadedFile() file: Express.Multer.File) {
const resized = await this.imageService.resizeImage(file.path);
return resized; // Returns binary image (set headers appropriately)
}
}
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('image')
export class ImageController {
constructor(private readonly imageService: ImageService) {}
@Post('resize')
@UseInterceptors(FileInterceptor('file'))
async resize(@UploadedFile() file: Express.Multer.File) {
const resized = await this.imageService.resizeImage(file.path);
return resized; // Returns binary image (set headers appropriately)
}
}
Result: Uploaded images are resized in parallel threads, keeping your API responsive.
Node.js Execution Model: The Basics
-
Node.js uses a single-threaded event loop for asynchronous I/O (like reading files, querying DBs, etc.).
-
It’s fast for I/O-bound tasks, but CPU-bound operations can block the loop and freeze the app.
To fix that, Node.js provides:
-
worker_threads – true multi-threading
-
cluster – process-level parallelism
-
child_process – isolated subprocess execution
When to Use What in NestJS
| Use Case |
Best Approach |
| CPU-intensive computation |
worker_threads |
| Scaling HTTP server |
cluster |
| Background processing or scripts |
child_process |
| Shared memory between threads |
worker_threads |
| Simple I/O tasks |
Stick to async/await |
| Use Case |
Best Approach |
| CPU-intensive computation |
worker_threads |
| Scaling HTTP server |
cluster |
| Background processing or scripts |
child_process |
| Shared memory between threads |
worker_threads |
| Simple I/O tasks |
Stick to async/await |
Hope you find it helpful!!