All posts
PerformanceJun 12, 2026·9 min read

Node.js Performance Tuning Guide

Profile before you optimize. This guide covers flame graphs, libuv thread pool tuning, cluster mode, memory leak detection, and the four most common performance antipatterns in Node.js APIs.

Measure before you optimize

The most common Node.js performance mistake is guessing. Engineers add caches, rewrite queries, or move to a different framework — without profiling first. Profile, find the bottleneck, fix the bottleneck, measure again. Repeat.

CPU profiling with --prof

# Run your app with profiling enabled
node --prof src/index.js

# Process the output
node --prof-process isolate-*.log > profile.txt

# Or use the V8 inspector
node --inspect src/index.js
# Open chrome://inspect in Chrome

Look for hot functions taking more than 5% of CPU time. Focus optimization there, not on cold paths.

Flame graphs

Use 0x (npm install -g 0x) to generate interactive SVG flame graphs. These are far easier to read than raw profiler output and immediately show you where time is being spent.

0x -- node src/index.js

The libuv thread pool

Node's thread pool handles file I/O, DNS lookups, and some crypto operations. The default pool size is 4. If your app does heavy concurrent file operations or DNS, increase it:

UV_THREADPOOL_SIZE=16 node src/index.js

Cluster mode

Node is single-threaded per process, but you can spawn one worker per CPU core using the cluster module or a process manager like PM2.

import cluster from 'cluster';
import os from 'os';

if (cluster.isPrimary) {
  const cpus = os.cpus().length;
  for (let i = 0; i < cpus; i++) cluster.fork();
} else {
  startServer();
}

The four most common antipatterns

1. Synchronous operations on the hot path. fs.readFileSync, JSON.parse on large payloads, and crypto.pbkdf2Sync block the event loop for every concurrent request. Use async alternatives.

2. N+1 queries. Fetching a list of items and then querying each item's related data in a loop. Use JOINs, DataLoader, or batch queries.

3. No connection pooling. Creating a new database connection per request instead of reusing a pool. This alone can make an app 20x slower under load.

4. Memory leaks through event emitters. Adding listeners inside request handlers without removing them. Use emitter.setMaxListeners and always clean up in the request lifecycle.

Memory leak detection

# Take a heap snapshot
node --expose-gc src/index.js

# In your health endpoint:
import v8 from 'v8';
app.get('/heap', (req, res) => {
  res.json(v8.getHeapStatistics());
});

Monitor heap growth over time. Steady growth under constant load is a leak. Use Chrome DevTools heap snapshots to compare allocations between time points.

Caching correctly

In-process caching (a simple Map with TTL) is faster than Redis for frequently read, rarely changing data. Use Redis when you need shared state across replicas or TTL-based eviction at scale.

HTTP keep-alive

Node's http.Agent doesn't enable keep-alive by default for outgoing requests. This means a new TCP connection per request to upstream services. Enable it:

import http from 'http';
const agent = new http.Agent({ keepAlive: true });
fetch('https://api.example.com', { agent });

Ready to put this into practice?

Deploy your Node.js app to production in minutes — zero YAML, automatic CI/CD, and HTTPS included.