DevToolBoxGRATIS
Blog

Node.js Performance Guide: Event Loop, Profiling, Memory Leaks, Worker Threads, and Benchmarking

18 min readby DevToolBox

Node.js Performance Guide: Event Loop, Profiling, and Optimization 2026

Master Node.js performance optimization with this comprehensive guide. Covers event loop internals, CPU and memory profiling with clinic.js and Chrome DevTools, flame graphs, worker_threads, cluster mode, caching strategies, connection pooling, HTTP/2, compression, benchmarking with autocannon, and a Node.js vs Bun vs Deno performance comparison.

TL;DR — Node.js Performance Quick Reference
  • Never block the event loop — use async I/O, streams, and worker_threads for CPU work.
  • Profile with node --inspect + Chrome DevTools or clinic.js for flamegraphs.
  • Detect memory leaks with heap snapshots; common causes are forgotten listeners and unbounded caches.
  • Use cluster module or PM2 cluster mode to utilize all CPU cores for HTTP servers.
  • Cache hot data in-process (lru-cache) and shared state in Redis; pool database connections.
  • Enable gzip/Brotli compression and HTTP/2 via Nginx in front of Node.js.
  • Benchmark with autocannon; measure p99 latency, not just average req/sec.

Why Node.js Performance Matters

Node.js is used by Netflix, LinkedIn, PayPal, and Uber to serve millions of requests per second. Its non-blocking, event-driven architecture makes it extremely efficient for I/O-bound workloads — but that same single-threaded model means a single blocking operation can freeze the entire server for all connected clients. Understanding Node.js performance is not optional; it is foundational to building reliable, scalable backend systems.

This guide goes deep on every layer of Node.js performance: the runtime internals, profiling tools, memory management, parallelism, caching, network-level optimizations, and benchmarking methodology. Whether you are debugging a slow API endpoint or designing a high-throughput microservice from scratch, the techniques here apply directly.

Key Takeaways
  • The event loop has 6 phases; microtasks (Promises, nextTick) run between each phase.
  • Blocking the event loop for more than ~10ms degrades all concurrent request latency.
  • CPU profiling with flame graphs is the fastest way to find hot paths in production.
  • Memory leaks in Node.js are usually event listeners, closures, or unbounded Maps/arrays.
  • Connection pooling reduces query latency from 50-100ms to under 1ms per call.
  • Bun is 2-4x faster than Node.js in microbenchmarks but architectural choices matter more.
  • PM2 cluster mode doubles throughput on a 2-core machine with a single config line.

Event Loop Deep Dive: Phases, Microtasks, and Macrotasks

The event loop is the heart of Node.js concurrency. Unlike multi-threaded servers that create an OS thread per connection, Node.js runs all JavaScript on a single thread. The event loop processes callbacks phase by phase, enabling non-blocking I/O without thread overhead.

The Six Event Loop Phases

Event Loop Iteration (one "tick"):

  ┌────────────────────────────────────────────┐
  │               Event Loop                  │
  │                                            │
  │  1. TIMERS          setTimeout / setInterval │
  │  2. PENDING CB      I/O errors from last tick│
  │  3. IDLE / PREPARE  internal use only       │
  │  4. POLL            fetch new I/O events    │
  │  5. CHECK           setImmediate()          │
  │  6. CLOSE CB        socket.on('close', ...) │
  │                                            │
  │  Between EVERY phase:                      │
  │    → drain process.nextTick queue          │
  │    → drain Promise microtask queue         │
  └────────────────────────────────────────────┘

Microtasks vs Macrotasks

// Execution order demonstration
console.log('1: sync start');

setTimeout(() => console.log('2: setTimeout (macrotask)'), 0);
setImmediate(() => console.log('3: setImmediate (macrotask)'));

Promise.resolve()
  .then(() => console.log('4: Promise.then (microtask)'))
  .then(() => console.log('5: Promise.then chained (microtask)'));

process.nextTick(() => console.log('6: nextTick (microtask, highest priority)'));
process.nextTick(() => console.log('7: nextTick second'));

queueMicrotask(() => console.log('8: queueMicrotask'));

console.log('9: sync end');

// Output:
// 1: sync start
// 9: sync end
// 6: nextTick (microtask, highest priority)
// 7: nextTick second
// 4: Promise.then (microtask)
// 5: Promise.then chained (microtask)
// 8: queueMicrotask
// 2: setTimeout (macrotask)   <- order vs setImmediate depends on poll phase timing
// 3: setImmediate (macrotask)

What Blocks the Event Loop

Any synchronous operation that takes a long time blocks every pending I/O callback, increasing latency for all users. Common culprits:

// ❌ Blocking the event loop — NEVER do this in a server
const crypto = require('crypto');
const fs = require('fs');

// 1. Synchronous file I/O
const data = fs.readFileSync('/large-file.csv');         // blocks during disk read

// 2. JSON parsing of huge objects
const obj = JSON.parse(hugeJsonString);                  // can take 50-500ms for 100MB+

// 3. RegExp catastrophic backtracking
/^(a+)+$/.test('aaaaaaaaaaaaaaaaaab');                  // exponential time

// 4. CPU-bound loops without yield
for (let i = 0; i < 1_000_000_000; i++) { /* work */ } // blocks 1-5 seconds

// 5. Synchronous crypto
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512'); // blocks!

// ✅ Non-blocking alternatives
const data = await fs.promises.readFile('/large-file.csv');

// Split large JSON work into chunks
function parseChunked(str) {
  return new Promise(resolve => setImmediate(() => resolve(JSON.parse(str))));
}

// Async crypto
const hash = await new Promise((resolve, reject) =>
  crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, key) =>
    err ? reject(err) : resolve(key)
  )
);

Measuring Event Loop Lag

// Monitor event loop delay in production
const { monitorEventLoopDelay } = require('perf_hooks');

const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();

setInterval(() => {
  const lag = histogram.mean / 1e6; // nanoseconds to milliseconds
  const p99 = histogram.percentile(99) / 1e6;

  if (lag > 50) {
    console.warn(`Event loop lag: mean=${lag.toFixed(2)}ms p99=${p99.toFixed(2)}ms`);
  }

  histogram.reset();
}, 5000);

// Simple lag sampler (lightweight alternative)
let lastCheck = Date.now();
setInterval(() => {
  const now = Date.now();
  const lag = now - lastCheck - 100; // 100ms interval
  if (lag > 20) console.warn(`Event loop lag: ${lag}ms`);
  lastCheck = now;
}, 100).unref(); // .unref() prevents this timer from keeping the process alive

Profiling Node.js Applications

Profiling is the process of measuring where your application spends time. Never optimize without profiling first — intuition is almost always wrong about hot paths.

Built-in: node --inspect with Chrome DevTools

# Start with inspector enabled
node --inspect server.js

# Break on first line (wait for debugger before executing)
node --inspect-brk server.js

# For running servers: send SIGUSR1 to activate inspector
kill -SIGUSR1 <pid>

# Then open: chrome://inspect in Chrome
# Click "inspect" under Remote Target
# Navigate to Performance tab → Record → Apply load → Stop

clinic.js — Automated Performance Analysis

# Install globally
npm install -g clinic

# clinic doctor: identifies root cause of performance issue
clinic doctor -- node server.js
# Generates HTML report: Is it CPU? I/O? Memory? Async?

# clinic flame: generates CPU flame graph
clinic flame -- node server.js
# Shows where CPU time is being spent as interactive SVG

# clinic bubbleprof: profiles async operations
clinic bubbleprof -- node server.js
# Shows async operation chains and their durations

# Run load while clinic is profiling
# (in a separate terminal)
autocannon -c 100 -d 30 http://localhost:3000
# Then Ctrl+C the clinic process to generate report

V8 CPU Profiler via --prof Flag

# Generate V8 profiler log
node --prof server.js

# Apply load, then stop server
# This creates a file: isolate-0x....-v8.log

# Process the log into human-readable format
node --prof-process isolate-*.log > profile.txt

# Key sections to check in profile.txt:
# [Summary] — time in JS, C++, GC, idle
# [Bottom up (heavy) profile] — hottest functions
# [JavaScript] section — your code's hot paths

0x — Single-Command Flame Graphs

# Install 0x (zero-x)
npm install -g 0x

# Profile and generate flamegraph in one command
0x server.js

# With existing running process (Linux only)
0x --pid <pid>

# Output: flamegraph.html — open in browser
# Hot frames appear wider; look for wide frames in YOUR code
# Dark orange/red = hot path (high CPU time)

Memory Management and Detecting Leaks

Memory leaks in Node.js cause heap growth over time, eventually leading to OOM (Out of Memory) crashes or degraded performance as garbage collection becomes more frequent.

Understanding V8 Heap Structure

// Check memory usage
const used = process.memoryUsage();
console.log({
  rss: Math.round(used.rss / 1024 / 1024) + ' MB',         // Resident Set Size
  heapTotal: Math.round(used.heapTotal / 1024 / 1024) + ' MB', // V8 heap allocated
  heapUsed: Math.round(used.heapUsed / 1024 / 1024) + ' MB',   // V8 heap in use
  external: Math.round(used.external / 1024 / 1024) + ' MB',   // C++ objects (Buffers)
  arrayBuffers: Math.round(used.arrayBuffers / 1024 / 1024) + ' MB',
});

// Monitor heap over time
setInterval(() => {
  const { heapUsed, heapTotal } = process.memoryUsage();
  const used = (heapUsed / 1024 / 1024).toFixed(1);
  const total = (heapTotal / 1024 / 1024).toFixed(1);
  console.log(`Heap: ${used}MB / ${total}MB`);
}, 10_000);

Taking Heap Snapshots

// Method 1: Built-in v8 module (Node.js 11.13+)
const v8 = require('v8');
const path = require('path');
const fs = require('fs');

function takeHeapSnapshot() {
  const filename = `heap-${Date.now()}.heapsnapshot`;
  const snapshotPath = path.join('/tmp', filename);
  const snapshot = v8.writeHeapSnapshot(snapshotPath);
  console.log('Heap snapshot written to:', snapshot);
  return snapshot;
}

// Take snapshot on demand via HTTP endpoint (useful in production)
app.get('/debug/heap', (req, res) => {
  const file = takeHeapSnapshot();
  res.download(file);
});

// Method 2: heapdump package (older approach)
// npm install heapdump
const heapdump = require('heapdump');
process.on('SIGUSR2', () => {
  heapdump.writeSnapshot((err, filename) => {
    if (!err) console.log('Heap snapshot:', filename);
  });
});
// Trigger: kill -USR2 <pid>

Common Memory Leak Patterns and Fixes

// ❌ LEAK 1: Unbounded cache (Map that only grows)
const cache = new Map();
app.get('/user/:id', async (req, res) => {
  if (!cache.has(req.params.id)) {
    cache.set(req.params.id, await db.getUser(req.params.id));
  }
  res.json(cache.get(req.params.id));
  // Problem: cache never evicts entries → unbounded memory
});

// ✅ Fix: Use LRU cache with size limit
const LRU = require('lru-cache');
const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 }); // 1000 items, 5min TTL

// ❌ LEAK 2: Event listener accumulation
function startPolling(emitter) {
  setInterval(() => emitter.emit('data', Date.now()), 1000);
  emitter.on('data', (ts) => processData(ts));
  // Called multiple times → listeners pile up
}

// ✅ Fix: Remove listener or use .once()
function startPolling(emitter) {
  const handler = (ts) => processData(ts);
  emitter.on('data', handler);
  return () => emitter.removeListener('data', handler); // return cleanup fn
}

// ❌ LEAK 3: Closure holding large buffer
function processRequest(largeBuffer) {
  const summary = computeSummary(largeBuffer);
  return {
    getSummary: () => summary,
    getOriginal: () => largeBuffer, // keeps largeBuffer in memory
  };
}

// ✅ Fix: Don't capture what you don't need
function processRequest(largeBuffer) {
  const summary = computeSummary(largeBuffer);
  // largeBuffer is now eligible for GC after this function returns
  return { getSummary: () => summary };
}

// ❌ LEAK 4: Global array accumulation
const requestLog = [];
app.use((req) => {
  requestLog.push({ url: req.url, time: Date.now() }); // grows forever
});

// ✅ Fix: Use circular buffer or stream to file
const MAX_LOG = 10000;
const requestLog = [];
app.use((req) => {
  requestLog.push({ url: req.url, time: Date.now() });
  if (requestLog.length > MAX_LOG) requestLog.shift(); // evict oldest
});

memwatch-next for Automatic Leak Detection

// npm install @airbnb/node-memwatch
const memwatch = require('@airbnb/node-memwatch');

// Alert when heap grows significantly
memwatch.on('leak', (info) => {
  console.error('Memory leak detected:', info);
  // info: { start, end, growth, reason }
});

// Diff two heap snapshots to find what grew
const hd = new memwatch.HeapDiff();
// ... do some work ...
const diff = hd.end();
console.log('Heap diff:', JSON.stringify(diff.change, null, 2));
// Shows which object types grew and by how much

CPU Profiling and Flame Graphs

A flame graph visualizes call stacks sampled during CPU profiling. Each box is a function; width represents the percentage of time spent in that function (including its callees). The color is arbitrary — look for wide plateaus in your own code.

Reading a Flame Graph

Flame Graph Reading Guide:

  Top    → most recently called function (on CPU right now)
  Bottom → entry point (main, HTTP handler, etc.)
  Width  → % of total CPU time (WIDE = slow = investigate!)
  Color  → arbitrary; orange = JS, green = C++, red = hot path (in some tools)

  Example pattern to look for:
  ┌─────────────────────────────────────────────────────┐
  │                 requestHandler                      │  ← wide = slow!
  │    dbQuery           │    serialize    │  validate  │
  │  pg.query │ JSON.str │ JSON.str       │             │
  └─────────────────────────────────────────────────────┘
                              ↑
              JSON.stringify appearing twice = optimization opportunity
              (cache the serialized result)

Optimizing Based on Flame Graph Findings

// Scenario: JSON.stringify is hot in flame graph
// ❌ Serializing the same data on every request
app.get('/config', (req, res) => {
  res.json(buildConfig()); // builds + serializes on every hit
});

// ✅ Serialize once, send string directly
let cachedConfig = null;
let cacheTime = 0;
const CACHE_TTL = 60_000; // 1 minute

app.get('/config', (req, res) => {
  const now = Date.now();
  if (!cachedConfig || now - cacheTime > CACHE_TTL) {
    cachedConfig = JSON.stringify(buildConfig());
    cacheTime = now;
  }
  res.setHeader('Content-Type', 'application/json');
  res.send(cachedConfig); // no serialization overhead
});

// Scenario: RegExp is hot
// ❌ Compiling RegExp on every call
function validate(email) {
  return /^[^s@]+@[^s@]+.[^s@]+$/.test(email); // recompiles each call
}

// ✅ Compile once at module level
const EMAIL_RE = /^[^s@]+@[^s@]+.[^s@]+$/;
function validate(email) {
  return EMAIL_RE.test(email);
}

// Scenario: Object creation in hot loop
// ❌ Creating temp objects in tight loop
function processItems(items) {
  return items.map(item => ({ ...item, processed: true, ts: Date.now() }));
}

// ✅ Reuse buffer array, minimize allocations
function processItems(items) {
  const result = new Array(items.length);
  const ts = Date.now(); // compute once
  for (let i = 0; i < items.length; i++) {
    result[i] = Object.assign({}, items[i], { processed: true, ts });
  }
  return result;
}

Cluster Module and worker_threads for CPU-Bound Tasks

Node.js is single-threaded, but modern servers have 4-128 CPU cores. Two mechanisms let you utilize them: the cluster module for scaling HTTP servers across cores, and worker_threads for parallelizing CPU-bound JavaScript within a single process.

Cluster Module

// cluster.js — Master/Worker HTTP server
const cluster = require('cluster');
const http = require('http');
const os = require('os');

const NUM_CPUS = os.cpus().length;

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} starting ${NUM_CPUS} workers`);

  // Fork one worker per CPU core
  for (let i = 0; i < NUM_CPUS; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.warn(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`);
    cluster.fork(); // automatic restart on crash
  });

  // Zero-downtime rolling restart
  process.on('SIGUSR2', () => {
    const workers = Object.values(cluster.workers);
    let i = 0;
    function restartNext() {
      if (i >= workers.length) return;
      const worker = workers[i++];
      worker.once('exit', () => {
        cluster.fork().once('listening', restartNext);
      });
      worker.disconnect();
    }
    restartNext();
  });

} else {
  // Worker process — each has its own event loop
  const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end(`Hello from worker ${process.pid}`);
  });

  server.listen(3000, () => {
    console.log(`Worker ${process.pid} listening on port 3000`);
  });
}

worker_threads for CPU-Intensive Work

// worker-pool.js — A simple worker thread pool
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const os = require('os');
const path = require('path');

class WorkerPool {
  constructor(workerScript, size = os.cpus().length) {
    this.workers = [];
    this.queue = [];
    this.workerScript = workerScript;

    for (let i = 0; i < size; i++) {
      this.addWorker();
    }
  }

  addWorker() {
    const worker = new Worker(this.workerScript);
    const state = { worker, idle: true };

    worker.on('message', (result) => {
      state.resolve(result);
      state.idle = true;
      this.processQueue();
    });

    worker.on('error', (err) => {
      state.reject(err);
      state.idle = true;
      this.processQueue();
    });

    this.workers.push(state);
  }

  processQueue() {
    if (this.queue.length === 0) return;
    const idleWorker = this.workers.find(w => w.idle);
    if (!idleWorker) return;

    const { data, resolve, reject } = this.queue.shift();
    idleWorker.idle = false;
    idleWorker.resolve = resolve;
    idleWorker.reject = reject;
    idleWorker.worker.postMessage(data);
  }

  run(data) {
    return new Promise((resolve, reject) => {
      this.queue.push({ data, resolve, reject });
      this.processQueue();
    });
  }
}

// cpu-task.worker.js — the worker script
if (!isMainThread) {
  parentPort.on('message', ({ type, payload }) => {
    let result;
    switch (type) {
      case 'fibonacci':
        result = fibonacci(payload.n);
        break;
      case 'hash':
        const crypto = require('crypto');
        result = crypto.createHash('sha256').update(payload.data).digest('hex');
        break;
    }
    parentPort.postMessage(result);
  });

  function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

// Usage in main thread
if (isMainThread) {
  const pool = new WorkerPool('./cpu-task.worker.js', 4);

  app.post('/hash', async (req, res) => {
    // Offload CPU work to worker; main thread stays free for other requests
    const hash = await pool.run({ type: 'hash', payload: { data: req.body.text } });
    res.json({ hash });
  });
}

SharedArrayBuffer for Zero-Copy Data Transfer

// Transfer large data between threads without copying
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // Allocate shared memory
  const sharedBuffer = new SharedArrayBuffer(4 * 1024 * 1024); // 4MB
  const view = new Int32Array(sharedBuffer);

  // Fill with data
  for (let i = 0; i < view.length; i++) view[i] = i;

  const worker = new Worker(__filename, {
    workerData: { sharedBuffer },
  });

  worker.on('message', (sum) => {
    console.log('Sum computed by worker:', sum);
  });

} else {
  // Worker reads from shared memory — no data copy!
  const view = new Int32Array(workerData.sharedBuffer);

  let sum = 0;
  for (let i = 0; i < view.length; i++) sum += view[i];

  parentPort.postMessage(sum);
}

Optimizing Async Code and Stream Processing

Poorly structured async code can serialize operations that could run in parallel, or process large datasets all at once instead of streaming. Both patterns cause unnecessary latency and memory pressure.

Parallel vs Sequential Async Operations

// ❌ Sequential (slow): 3 × 100ms = 300ms total
async function getUserData(userId) {
  const user = await db.getUser(userId);           // 100ms
  const orders = await db.getOrders(userId);       // 100ms
  const preferences = await db.getPrefs(userId);  // 100ms
  return { user, orders, preferences };
}

// ✅ Parallel (fast): max(100ms, 100ms, 100ms) = 100ms total
async function getUserData(userId) {
  const [user, orders, preferences] = await Promise.all([
    db.getUser(userId),
    db.getOrders(userId),
    db.getPrefs(userId),
  ]);
  return { user, orders, preferences };
}

// ✅ Parallel with error handling (one failure doesn't abort others)
async function getUserData(userId) {
  const results = await Promise.allSettled([
    db.getUser(userId),
    db.getOrders(userId),
    db.getPrefs(userId),
  ]);
  return results.map(r => r.status === 'fulfilled' ? r.value : null);
}

// ❌ Promise.all on large array: all N requests fire simultaneously
const users = await Promise.all(userIds.map(id => db.getUser(id)));
// If userIds has 10,000 items, this opens 10,000 DB connections at once!

// ✅ Batched concurrency with p-limit
const pLimit = require('p-limit');
const limit = pLimit(20); // max 20 concurrent operations

const users = await Promise.all(
  userIds.map(id => limit(() => db.getUser(id)))
);

Stream Processing for Large Datasets

const fs = require('fs');
const { Transform, pipeline } = require('stream');
const { promisify } = require('util');
const pipelineAsync = promisify(pipeline);

// ❌ Load entire file into memory (OOM risk for large files)
async function processLargeFile(filePath) {
  const content = await fs.promises.readFile(filePath, 'utf8'); // could be 2GB!
  const lines = content.split('
');
  return lines.map(processLine);
}

// ✅ Stream line by line — constant memory usage regardless of file size
async function processLargeFile(filePath, outputPath) {
  let lineCount = 0;
  let buffer = '';

  const lineProcessor = new Transform({
    transform(chunk, encoding, callback) {
      buffer += chunk.toString();
      const lines = buffer.split('
');
      buffer = lines.pop(); // keep incomplete last line

      for (const line of lines) {
        if (line.trim()) {
          lineCount++;
          this.push(processLine(line) + '
');
        }
      }
      callback();
    },
    flush(callback) {
      if (buffer.trim()) {
        this.push(processLine(buffer) + '
');
      }
      callback();
    },
  });

  await pipelineAsync(
    fs.createReadStream(filePath),
    lineProcessor,
    fs.createWriteStream(outputPath)
  );

  return lineCount;
}

// ✅ HTTP response streaming — send data as it's ready
app.get('/large-data', async (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.write('[');

  const cursor = db.collection('events').find().cursor();
  let first = true;

  for await (const doc of cursor) {
    if (!first) res.write(',');
    res.write(JSON.stringify(doc));
    first = false;
  }

  res.write(']');
  res.end();
});

Caching Strategies

Caching is the single most impactful optimization for most Node.js applications. A database query that takes 20ms can become a sub-millisecond in-memory lookup. The key is choosing the right cache layer for each type of data.

In-Memory Caching with lru-cache

// npm install lru-cache
const { LRUCache } = require('lru-cache');

// Cache configuration
const userCache = new LRUCache({
  max: 5000,              // maximum number of items
  ttl: 1000 * 60 * 5,    // 5 minutes TTL
  updateAgeOnGet: true,   // reset TTL on access
  updateAgeOnHas: false,
  // Optional: size-based eviction
  maxSize: 50_000_000,    // 50MB max cache size
  sizeCalculation: (value) => JSON.stringify(value).length,
});

// Cache-aside pattern
async function getUser(userId) {
  const cached = userCache.get(userId);
  if (cached) return cached;

  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
  userCache.set(userId, user);
  return user;
}

// Invalidate on mutation
async function updateUser(userId, data) {
  await db.query('UPDATE users SET ... WHERE id = $1', [userId]);
  userCache.delete(userId); // invalidate cache entry
}

// Cache function results (memoization)
function memoize(fn, cache = new LRUCache({ max: 1000, ttl: 60_000 })) {
  return async function memoized(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = await fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const cachedFetchWeather = memoize(fetchWeather);

Redis Caching with ioredis

// npm install ioredis
const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: 6379,
  maxRetriesPerRequest: 3,
  retryStrategy: (times) => Math.min(times * 50, 2000),
  lazyConnect: true,
});

// Basic cache-aside with Redis
async function getCachedUser(userId) {
  const key = `user:${userId}`;

  // Try Redis first
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  // Fall back to DB
  const user = await db.getUser(userId);

  // Cache for 5 minutes (EX = seconds)
  await redis.set(key, JSON.stringify(user), 'EX', 300);

  return user;
}

// Atomic cache update (SET NX = only set if not exists)
async function acquireLock(lockKey, ttlSeconds = 30) {
  const result = await redis.set(lockKey, '1', 'NX', 'EX', ttlSeconds);
  return result === 'OK'; // true if lock acquired
}

// Redis pipelining: batch multiple commands in one round trip
async function getMultipleUsers(userIds) {
  const pipeline = redis.pipeline();
  userIds.forEach(id => pipeline.get(`user:${id}`));
  const results = await pipeline.exec();
  // results: [[null, value1], [null, value2], ...]
  return results.map(([err, val]) => val ? JSON.parse(val) : null);
}

// Cache invalidation by pattern (use sparingly — SCAN, not KEYS)
async function invalidateUserCache(userId) {
  const pattern = `user:${userId}*`;
  let cursor = '0';
  do {
    const [newCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
    cursor = newCursor;
    if (keys.length) await redis.del(...keys);
  } while (cursor !== '0');
}

HTTP Response Caching

const express = require('express');
const app = express();

// Cache-Control headers strategy
app.get('/static-data', (req, res) => {
  res.setHeader('Cache-Control', 'public, max-age=86400, immutable');
  // Browser + CDN cache for 24 hours
  res.json(getStaticData());
});

app.get('/api/users', (req, res) => {
  res.setHeader('Cache-Control', 'private, max-age=60');
  // Only browser caches; 60 seconds
  res.json(getUsers());
});

app.get('/api/realtime', (req, res) => {
  res.setHeader('Cache-Control', 'no-store');
  // Never cache
  res.json(getRealtimeData());
});

// ETag for conditional requests
app.get('/api/product/:id', async (req, res) => {
  const product = await db.getProduct(req.params.id);
  const etag = `"${product.updatedAt.getTime()}"`;

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // Not Modified — no body sent
  }

  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
  res.json(product);
});

Connection Pooling for Databases

Every database query over a new TCP connection incurs a TCP handshake (~1ms on LAN), TLS handshake (~5-10ms), and database authentication (~5-50ms). Connection pooling eliminates this overhead by reusing existing authenticated connections.

PostgreSQL Connection Pool (node-postgres)

// npm install pg
const { Pool } = require('pg');

// Create pool once at startup — never recreate per request
const pool = new Pool({
  host: process.env.DB_HOST,
  port: 5432,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,

  // Pool configuration
  min: 2,         // Keep at least 2 connections open
  max: 20,        // Never exceed 20 connections
  idleTimeoutMillis: 30_000,    // Close idle connections after 30s
  connectionTimeoutMillis: 5_000, // Fail fast if pool is exhausted

  // SSL for production
  ssl: process.env.NODE_ENV === 'production'
    ? { rejectUnauthorized: true, ca: process.env.DB_CA_CERT }
    : false,
});

// Monitor pool health
pool.on('connect', (client) => {
  console.log('New client connected to PostgreSQL');
});
pool.on('error', (err, client) => {
  console.error('Unexpected error on idle client', err);
});

// Helper with automatic connection management
async function query(text, params) {
  const start = Date.now();
  const result = await pool.query(text, params);
  const duration = Date.now() - start;

  if (duration > 1000) {
    console.warn('Slow query detected:', { text, duration, rows: result.rowCount });
  }

  return result;
}

// For transactions: use a dedicated client
async function transferFunds(fromId, toId, amount) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
      [amount, fromId]
    );
    await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
      [amount, toId]
    );
    await client.query('COMMIT');
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release(); // CRITICAL: always release back to pool
  }
}

MySQL Connection Pool

// npm install mysql2
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 20,
  queueLimit: 0,       // 0 = unlimited queue
  connectTimeout: 10_000,
  idleTimeout: 60_000,
});

// Use pool directly — no manual connection management for simple queries
const [rows] = await pool.execute(
  'SELECT * FROM users WHERE id = ?', [userId]
);

// For prepared statements (better performance for repeated queries)
const [rows] = await pool.execute(
  'SELECT * FROM orders WHERE user_id = ? AND status = ?',
  [userId, 'active']
);

MongoDB Connection Pool

// npm install mongodb
const { MongoClient } = require('mongodb');

let client;

async function getMongoClient() {
  if (!client) {
    client = new MongoClient(process.env.MONGODB_URI, {
      // Pool configuration
      minPoolSize: 5,
      maxPoolSize: 50,
      maxIdleTimeMS: 30_000,
      serverSelectionTimeoutMS: 5_000,
      socketTimeoutMS: 45_000,

      // Compression to reduce network I/O
      compressors: ['snappy', 'zlib'],
    });
    await client.connect();
  }
  return client;
}

// Usage
async function findUser(userId) {
  const mongoClient = await getMongoClient();
  const db = mongoClient.db('myapp');
  return db.collection('users').findOne({ _id: userId });
}

// Graceful shutdown
process.on('SIGTERM', async () => {
  if (client) await client.close();
  process.exit(0);
});

PM2 Cluster Mode vs Single Process

PM2 is the standard Node.js process manager for production deployments. Its cluster mode leverages the Node.js cluster module automatically, distributing incoming connections across multiple worker processes.

PM2 Ecosystem Configuration

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'api-server',
      script: 'dist/server.js',        // compiled output
      cwd: '/var/www/myapp',

      // Cluster mode: runs one instance per CPU core
      instances: 'max',                // or a number like 4
      exec_mode: 'cluster',            // vs 'fork' (single process)

      // Memory management
      max_memory_restart: '512M',      // restart if heap exceeds 512MB

      // Zero-downtime reload
      wait_ready: true,                // wait for app.send('ready')
      listen_timeout: 10000,           // ms before considering restart failed
      kill_timeout: 5000,              // ms before force-killing

      // Environment
      env: {
        NODE_ENV: 'development',
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
        UV_THREADPOOL_SIZE: 8,         // increase libuv thread pool
      },

      // Logging
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      error_file: '/var/log/myapp/error.log',
      out_file: '/var/log/myapp/out.log',
      merge_logs: true,                // merge all instance logs

      // Auto-restart on file change (dev only)
      watch: false,
      ignore_watch: ['node_modules', 'logs'],
    },
  ],
};
# Start in production mode
pm2 start ecosystem.config.js --env production

# Reload all workers with zero downtime (rolling restart)
pm2 reload api-server

# Check cluster status
pm2 status
pm2 describe api-server

# Monitor CPU and memory in real time
pm2 monit

# View logs
pm2 logs api-server --lines 100

# Save process list and enable startup on boot
pm2 startup
pm2 save

# Scale up/down without downtime
pm2 scale api-server 8    # scale to 8 instances
pm2 scale api-server +2   # add 2 more instances

Single Process vs Cluster Comparison

Node.js Process Mode Comparison:

                    Fork (single)    Cluster (max)    cluster+sticky
Cores used         1               all CPUs          all CPUs
Requests/sec       ~10k            ~10k × CPU count  ~10k × CPU count
Memory overhead    base            base × N workers  base × N workers
WebSocket support  ✓               ✗ (round-robin)   ✓ (sticky sessions)
Shared memory      N/A             via Redis         via Redis
Session state      in-process      must use Redis    must use Redis
Good for           dev, serverless HTTP APIs          WS, real-time apps

HTTP/2 and Compression

Network-level optimizations can dramatically reduce response times, especially for high-latency connections. HTTP/2 multiplexing eliminates head-of-line blocking, and compression reduces payload sizes by 60-90% for JSON and HTML.

HTTP/2 with Node.js

// Native HTTP/2 server (Node.js built-in)
const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('/etc/ssl/private.key'),
  cert: fs.readFileSync('/etc/ssl/certificate.crt'),

  // HTTP/2 settings
  settings: {
    headerTableSize: 4096,
    enablePush: true,
    initialWindowSize: 65535,
    maxFrameSize: 16384,
    maxConcurrentStreams: 1000,
  },
});

server.on('stream', (stream, headers) => {
  const path = headers[':path'];

  if (path === '/api/data') {
    stream.respond({ ':status': 200, 'content-type': 'application/json' });
    stream.end(JSON.stringify({ data: 'hello http2' }));
  }

  // HTTP/2 Server Push — push CSS before browser asks for it
  if (path === '/index.html') {
    stream.pushStream({ ':path': '/styles.css' }, (err, pushStream) => {
      if (err) return;
      pushStream.respond({ ':status': 200, 'content-type': 'text/css' });
      fs.createReadStream('./public/styles.css').pipe(pushStream);
    });

    stream.respond({ ':status': 200, 'content-type': 'text/html' });
    fs.createReadStream('./public/index.html').pipe(stream);
  }
});

server.listen(443);

Nginx as HTTP/2 Terminator (Recommended)

# /etc/nginx/sites-available/myapp.conf
server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    # Modern TLS configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    # Compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript
               application/x-javascript text/xml application/xml
               application/xml+rss text/javascript image/svg+xml;
    gzip_comp_level 6;      # 1-9; 6 is sweet spot

    # Brotli (better compression than gzip, requires ngx_brotli module)
    brotli on;
    brotli_comp_level 6;
    brotli_types text/plain text/css application/json application/javascript;

    # Upstream connection pool to Node.js
    upstream nodejs {
        least_conn;                    # route to least-busy worker
        keepalive 64;                  # keep 64 connections to Node.js open
        server 127.0.0.1:3000;
        server 127.0.0.1:3001;
        server 127.0.0.1:3002;
        server 127.0.0.1:3003;
    }

    location /api/ {
        proxy_pass http://nodejs;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_cache_bypass $http_upgrade;

        # Performance headers
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }
}

Compression in Node.js/Express

// npm install compression
const compression = require('compression');
const express = require('express');
const app = express();

// Enable gzip/deflate compression for all responses
app.use(compression({
  level: 6,             // 0-9 (6 is default sweet spot)
  threshold: 1024,      // Only compress responses > 1KB
  filter: (req, res) => {
    if (req.headers['x-no-compression']) return false;
    return compression.filter(req, res);
  },
}));

// Manual Brotli/gzip with Accept-Encoding negotiation
const zlib = require('zlib');

app.get('/large-data', async (req, res) => {
  const data = JSON.stringify(await getLargeDataset());
  const acceptEncoding = req.headers['accept-encoding'] || '';

  if (/br/.test(acceptEncoding)) {
    res.setHeader('Content-Encoding', 'br');
    res.setHeader('Content-Type', 'application/json');
    zlib.brotliCompress(data, (err, compressed) => {
      if (err) return res.json(JSON.parse(data));
      res.send(compressed);
    });
  } else if (/gzip/.test(acceptEncoding)) {
    res.setHeader('Content-Encoding', 'gzip');
    res.setHeader('Content-Type', 'application/json');
    zlib.gzip(data, (err, compressed) => {
      if (err) return res.json(JSON.parse(data));
      res.send(compressed);
    });
  } else {
    res.json(JSON.parse(data));
  }
});

Node.js Benchmarking with autocannon and wrk

Benchmarking provides objective data about your application's throughput and latency characteristics. Focus on percentile latency (p95, p99) rather than just average — your slowest users determine the true user experience.

autocannon

# Install autocannon
npm install -g autocannon

# Basic benchmark: 100 concurrent connections, 30 seconds
autocannon -c 100 -d 30 http://localhost:3000/api/users

# Output:
# Running 30s test @ http://localhost:3000/api/users
# 100 connections
#
# Stat         Avg      Stdev    Max
# Latency      2.04 ms  0.61 ms  18 ms
# Req/Sec      48312.8  2245.13  51200
# Bytes/Sec    8.45 MB  392 kB   8.97 MB
#
# 1449k requests in 30.01s, 253 MB read

# With POST body and custom headers
autocannon -c 50 -d 20   -m POST   -H 'Content-Type: application/json'   -b '{"email":"test@example.com","password":"secret"}'   http://localhost:3000/api/login

# Compare before/after optimization
autocannon -c 100 -d 30 --json http://localhost:3000 > before.json
# ... make changes ...
autocannon -c 100 -d 30 --json http://localhost:3000 > after.json

# Multiple endpoints in sequence
autocannon -c 100 -d 10 http://localhost:3000/api/a
autocannon -c 100 -d 10 http://localhost:3000/api/b

autocannon Programmatic API

// npm install autocannon
const autocannon = require('autocannon');

async function benchmark(url, options = {}) {
  const result = await autocannon({
    url,
    connections: 100,
    duration: 30,
    pipelining: 1,
    ...options,
  });

  console.table({
    'Requests/sec': result.requests.average,
    'Latency p50 (ms)': result.latency.p50,
    'Latency p95 (ms)': result.latency.p95,
    'Latency p99 (ms)': result.latency.p99,
    'Latency max (ms)': result.latency.max,
    'Throughput (MB/s)': (result.throughput.average / 1024 / 1024).toFixed(2),
    'Errors': result.errors,
    '2xx responses': result['2xx'],
    'Non-2xx': result.non2xx,
  });

  return result;
}

// Run benchmark and fail CI if performance regresses
async function runPerfTest() {
  const result = await benchmark('http://localhost:3000/api/health');

  const P99_THRESHOLD = 50;  // 50ms p99 SLA
  const RPS_THRESHOLD = 5000; // minimum 5k req/sec

  if (result.latency.p99 > P99_THRESHOLD) {
    throw new Error(`p99 latency ${result.latency.p99}ms exceeds threshold ${P99_THRESHOLD}ms`);
  }
  if (result.requests.average < RPS_THRESHOLD) {
    throw new Error(`Throughput ${result.requests.average} rps below threshold ${RPS_THRESHOLD} rps`);
  }

  console.log('Performance test PASSED');
}

runPerfTest().catch(err => { console.error(err); process.exit(1); });

wrk and wrk2

# Install wrk (macOS)
brew install wrk

# Basic wrk test
wrk -t12 -c400 -d30s http://localhost:3000/api/users
# -t: threads (use number of CPU cores)
# -c: connections
# -d: duration

# wrk with Lua script for custom requests
cat > post.lua << 'EOF'
wrk.method = "POST"
wrk.body   = '{"user":"test"}'
wrk.headers["Content-Type"] = "application/json"
EOF

wrk -t4 -c100 -d30s -s post.lua http://localhost:3000/api/users

# wrk2 for constant-rate testing (measures coordinated omission correctly)
# install from https://github.com/giltene/wrk2
wrk2 -t4 -c100 -d30s -R10000 http://localhost:3000
# -R: target request rate (10k rps)
# Reports HDR histogram for accurate p99.9 latency

Common Performance Anti-Patterns

These patterns are frequently found in Node.js codebases and have a measurable negative impact on performance. Recognize them in code reviews and refactor aggressively.

// ❌ ANTI-PATTERN 1: N+1 queries
// Fetches 1 order list + N separate user queries
app.get('/orders', async (req, res) => {
  const orders = await db.query('SELECT * FROM orders');
  const enriched = await Promise.all(
    orders.map(async order => ({
      ...order,
      user: await db.query('SELECT * FROM users WHERE id = $1', [order.userId]),
    }))
  );
  res.json(enriched);
});

// ✅ Fix: JOIN or batch fetch
app.get('/orders', async (req, res) => {
  const orders = await db.query(`
    SELECT o.*, u.name, u.email
    FROM orders o
    JOIN users u ON o.user_id = u.id
  `);
  res.json(orders.rows);
});

// ❌ ANTI-PATTERN 2: Awaiting in a loop (serial execution)
async function sendNotifications(userIds) {
  for (const id of userIds) {
    await emailService.send(id); // sends one at a time!
  }
}

// ✅ Fix: parallel with concurrency limit
const pLimit = require('p-limit');
const limit = pLimit(10);

async function sendNotifications(userIds) {
  await Promise.all(userIds.map(id => limit(() => emailService.send(id))));
}

// ❌ ANTI-PATTERN 3: Creating a new DB client per request
app.get('/user/:id', async (req, res) => {
  const client = new MongoClient(process.env.MONGODB_URI);
  await client.connect(); // new TCP + auth handshake every request!
  const user = await client.db('app').collection('users').findOne({ _id: req.params.id });
  await client.close();
  res.json(user);
});

// ✅ Fix: Create client once at startup (see connection pooling section)

// ❌ ANTI-PATTERN 4: Synchronous JSON stringify for large objects in hot path
app.get('/report', async (req, res) => {
  const report = await generateFullReport(); // 5MB object
  res.json(report); // synchronously serializes 5MB, blocks event loop!
});

// ✅ Fix: Stream the JSON response or use a streaming serializer
const { Readable } = require('stream');
const JSONStream = require('jsonstream'); // npm install JSONStream

app.get('/report', async (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  const cursor = db.collection('report_items').find();
  cursor.pipe(JSONStream.stringify()).pipe(res);
});

// ❌ ANTI-PATTERN 5: Missing index on queried fields
// SELECT * FROM users WHERE email = 'x@y.com' — full table scan without index!

// ✅ Fix: Always index columns used in WHERE, JOIN, ORDER BY
// PostgreSQL:
// CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
// MongoDB:
// db.users.createIndex({ email: 1 }, { unique: true });

// ❌ ANTI-PATTERN 6: Ignoring backpressure in streams
readable.on('data', (chunk) => {
  writable.write(chunk); // may write faster than consumer can handle
});

// ✅ Fix: Use pipeline or handle backpressure
const { pipeline } = require('stream/promises');
await pipeline(readable, transform, writable);

Comparison: Node.js vs Bun vs Deno Performance

All three runtimes have improved significantly in 2026. Here is a realistic comparison based on community benchmarks and production reports. Remember: runtime speed is rarely the bottleneck in real applications — database I/O and business logic dominate.

BenchmarkNode.js 22Deno 2.xBun 1.x
HTTP hello world (req/sec)~85k~130k (1.5x)~250k (3x)
JSON parse/stringifyBaseline~10% faster~30% faster
Startup time~50ms~30ms~5ms
npm package installBaseline (npm)~10% faster10-30x faster
File I/O throughputBaseline~5% faster~20% faster
Memory baseline~35MB~40MB~25MB
npm ecosystem compat100%~95%~98%
TypeScript nativePartial (v22)FullFull
Real-world app perf deltaBaseline5-15% faster10-20% faster
Production maturityExcellentGoodGood
Best use caseEnterprise, large teamsTypeScript-first APIsNew projects, CLIs

When the Runtime Choice Actually Matters

Runtime performance matters most for:
  ✓ CLI tools with fast startup (Bun wins significantly)
  ✓ Serverless functions (cold start time → Bun/Deno)
  ✓ CPU-bound computation (Bun's JavaScriptCore is faster for some ops)
  ✓ High-fan-out HTTP proxies with minimal business logic

Runtime performance matters least for:
  ✗ APIs backed by a database (DB latency >> runtime overhead)
  ✗ Applications with in-process caching (cache hits dominate)
  ✗ Microservices with substantial business logic
  ✗ Long-lived processes (startup time irrelevant)

Rule of thumb:
  If DB query = 5ms and your Node.js overhead = 0.1ms,
  switching to Bun (overhead ~0.03ms) saves 0.07ms per request.
  Fixing the missing index (5ms → 0.1ms) saves 4.9ms per request.

  Profile and optimize your own code first.

Performance Monitoring in Production

// Minimal production performance instrumentation
const { performance, PerformanceObserver } = require('perf_hooks');

// 1. Time async operations
async function timedDbQuery(sql, params) {
  const start = performance.now();
  try {
    const result = await pool.query(sql, params);
    const duration = performance.now() - start;

    // Log slow queries
    if (duration > 100) {
      console.warn(`Slow query (${duration.toFixed(1)}ms): ${sql.substring(0, 100)}`);
    }

    // Report to APM (Datadog, New Relic, etc.)
    metrics.histogram('db.query.duration', duration, { sql: sql.split(' ')[0] });

    return result;
  } catch (err) {
    metrics.increment('db.query.error');
    throw err;
  }
}

// 2. Express middleware for request timing
app.use((req, res, next) => {
  const start = process.hrtime.bigint();

  res.on('finish', () => {
    const duration = Number(process.hrtime.bigint() - start) / 1e6; // ms
    const route = req.route ? req.route.path : req.path;

    metrics.histogram('http.request.duration', duration, {
      method: req.method,
      route,
      status: res.statusCode,
    });

    if (duration > 1000) {
      console.warn(`Slow request: ${req.method} ${req.path} ${duration.toFixed(0)}ms`);
    }
  });

  next();
});

// 3. GC monitoring
const gcStats = { count: 0, totalDuration: 0 };
const gcObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    gcStats.count++;
    gcStats.totalDuration += entry.duration;
    if (entry.duration > 100) {
      console.warn(`GC pause: ${entry.detail.type} ${entry.duration.toFixed(1)}ms`);
    }
  }
});
gcObserver.observe({ type: 'gc', buffered: true });

// Report GC metrics every minute
setInterval(() => {
  metrics.gauge('gc.count', gcStats.count);
  metrics.gauge('gc.total_duration_ms', gcStats.totalDuration);
  gcStats.count = 0;
  gcStats.totalDuration = 0;
}, 60_000);

FAQ

How do I profile a Node.js application to find performance bottlenecks?

Use the built-in --inspect flag to connect Chrome DevTools for CPU profiling and flame graphs. Run node --inspect-brk server.js, open chrome://inspect, and start a recording in the Performance tab. For production profiling, use clinic.js (npm install -g clinic), which provides clinic doctor,clinic flame, and clinic bubbleprof sub-commands. The clinic doctor command identifies the most likely root cause automatically.

What are the phases of the Node.js event loop?

The Node.js event loop has six phases: (1) Timers — executes setTimeout and setInterval callbacks; (2) Pending callbacks — I/O callbacks deferred to the next loop; (3) Idle/Prepare — internal use only; (4) Poll — retrieves new I/O events and executes callbacks; (5) Check — executes setImmediate callbacks; (6) Close callbacks. Microtasks (Promise.then, process.nextTick) run between every phase transition, with process.nextTick having higher priority than Promise callbacks.

How do I detect and fix memory leaks in Node.js?

Monitor heap growth over time with process.memoryUsage(). When you suspect a leak, take heap snapshots with v8.writeHeapSnapshot() before and after a period of high load, then compare them in Chrome DevTools Memory tab — objects that grew indicate the leak source. Common causes are forgotten event listeners, unbounded Map/array caches, and closures holding references to large objects.

When should I use worker_threads vs the cluster module?

Use worker_threads for CPU-intensive JavaScript work within a single process — image processing, cryptography, complex calculations — where you want to share memory via SharedArrayBuffer. Use the cluster module (or PM2 cluster mode) to run multiple Node.js processes across all CPU cores, each with its own memory, for scaling HTTP servers. For most web APIs, PM2 cluster mode is simpler; use worker_threads only when you need parallelism within a single request handler.

How does caching improve Node.js performance?

Caching avoids redundant database queries and computations. In-process LRU caches (lru-cache) serve data in under 0.1ms vs 5-50ms for a database query. Redis is the standard for distributed caching across multiple Node.js instances. HTTP caching with Cache-Control headers lets browsers and CDNs serve responses without hitting your server at all. A typical production stack combines all three layers.

How do I benchmark a Node.js HTTP server?

Use autocannon (npm install -g autocannon): runautocannon -c 100 -d 30 http://localhost:3000 for 100 concurrent connections over 30 seconds. It reports req/sec, latency percentiles (p50/p95/p99), and throughput. Always warm up the server for 10-15 seconds before measuring, use realistic payload sizes, and focus on p99 latency rather than average — your slowest users define the experience.

What is connection pooling and why is it important?

Connection pooling reuses existing authenticated database connections instead of creating a new TCP + TLS + auth handshake on every query. A new connection costs 5-100ms; a pooled connection costs under 1ms. For PostgreSQL use pg.Pool, for MySQL usemysql2.createPool, and MongoDB manages its own pool internally. Configure pool size based on your database server's max_connections and your application's concurrency needs.

Is Node.js fast enough compared to Bun or Deno?

In raw HTTP benchmarks, Bun is 2-4x faster than Node.js and Deno is 1.5-2x faster. However, real-world differences are much smaller because database I/O, caching, and business logic dominate response time. For most production applications backed by a database, switching runtimes yields less than 10% improvement. Optimizing queries, adding indexes, and implementing caching will deliver 10-100x more impact than changing the runtime. Choose Node.js for its ecosystem maturity and LTS guarantees; consider Bun for CLI tools where startup time matters.

𝕏 Twitterin LinkedIn
Was dit nuttig?

Blijf op de hoogte

Ontvang wekelijkse dev-tips en nieuwe tools.

Geen spam. Altijd opzegbaar.

Try These Related Tools

{ }JSON Formatter.*Regex TesterB→Base64 Encoder

Related Articles

Node.js Guide: Complete Tutorial for Backend Development

Master Node.js backend development. Covers event loop, Express.js, REST APIs, authentication with JWT, database integration, testing with Jest, PM2 deployment, and Node.js vs Deno vs Bun comparison.

Express.js Guide: Routing, Middleware, REST APIs, and Authentication

Master Express.js for Node.js web development. Covers routing, middleware, building REST APIs with CRUD, JWT authentication, error handling, and Express vs Fastify vs Koa vs Hapi comparison.

Redis Complete Guide: Caching, Pub/Sub, Streams, and Production Patterns

Master Redis with this complete guide. Covers data types, Node.js ioredis, caching patterns, session storage, Pub/Sub, Streams, Python redis-py, rate limiting, transactions, and production setup.