skip to content
logo
Table of Contents

Key Performance Bottlenecks in Node.js

Optimizing a Node.js application means reducing latency, improving resource utilization, and eliminating bottlenecks. This post covers:

  • CPU-bound tasks
  • Event loop delays
  • Memory leaks
  • Profiling for bottlenecks

CPU-Intensive Tasks: Use Worker Threads

Blocking the main thread with CPU-heavy computations slows down request handling. Offload CPU-intensive tasks using worker threads.

Worker Thread Example

import { Worker } from "worker_threads";
const worker = new Worker("./worker.js");
worker.on("message", (msg) => console.log(`Worker response: ${msg}`));
worker.on("error", (err) => console.error("Worker error:", err));

Inside worker.js:

import { parentPort } from "worker_threads";
if (parentPort) {
const result = heavyComputation();
parentPort.postMessage(result);
}
function heavyComputation() {
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
return sum;
}

For distributed workloads, consider Redis queues (BullMQ) or Kafka.


Avoid Blocking the Event Loop

Node.js uses a single-threaded event loop, meaning a blocking operation delays all requests.

Identify Event Loop Delays

Use perf_hooks to monitor delays:

import { performance, PerformanceObserver } from "perf_hooks";
const obs = new PerformanceObserver((items) => {
console.log(items.getEntries());
});
obs.observe({ entryTypes: ["function"] });
performance.mark("start");
// Simulate blocking operation
for (let i = 0; i < 1e9; i++) {}
performance.mark("end");
performance.measure("Blocking Task", "start", "end");

Instead of blocking loops, use asynchronous patterns (async/await, Promises, streams).


Memory Leaks: Detect & Fix

Leaks cause high memory usage and slow down performance over time.

Common Causes:

  1. Global variables
  2. Unclosed event listeners
  3. Unmanaged caches

Debugging Memory Usage

Run Node.js with the --inspect flag and open Chrome DevTools.

Terminal window
node --inspect app.js

Heap Snapshot Debugging

const heap = process.memoryUsage();
console.log(`Heap Used: ${heap.heapUsed / 1024 / 1024} MB`);

To fix memory issues, ensure event listeners are removed and use WeakMaps for caching.


Optimize Database Queries

Poor database queries slow response times. Optimize with:

1. Connection Pooling

import { Pool } from "pg";
const pool = new Pool({ max: 20 });
const result = await pool.query("SELECT \* FROM users WHERE id = $1", [42]);
console.log(result.rows);

2. Use Indexing (PostgreSQL)

CREATE INDEX idx_users_id ON users (id);

3. Enable Query Caching

import { createClient } from "redis";
const redisClient = createClient();
await redisClient.connect();
const cacheKey = `user:42`;
const cachedData = await redisClient.get(cacheKey);
if (!cachedData) {
const user = await pool.query("SELECT \* FROM users WHERE id = $1", [42]);
await redisClient.setEx(cacheKey, 3600, JSON.stringify(user.rows[0]));
}

Profiling & Performance Monitoring

Use profiling tools to identify slow functions.

1. Use Node.js Built-in Profiler

Terminal window
node --prof app.js
node --prof-process isolate-\*.log

2. Monitor with Prometheus & Grafana

import promClient from "prom-client";
const httpRequestDurationMicroseconds = new promClient.Histogram({
name: "http_request_duration",
help: "Duration of HTTP requests in microseconds",
labelNames: ["method", "route", "status_code"],
buckets: [0.1, 5, 15, 50, 100, 500],
});

Key Takeaways

  • Use worker threads for CPU-heavy tasks.
  • Avoid blocking the event loop.
  • Detect memory leaks and fix inefficient caching.
  • Optimize database queries for speed.
  • Use profiling tools to find slow processes.

Efficient performance tuning requires monitoring, debugging, and testing. Implement these strategies to make your Node.js apps faster and more scalable.