Understanding Node.js From the Inside Out

Node.js is not just "JavaScript on the server." It is an ecosystem of powerful components — V8, libuv, the event loop, worker threads, and streams — all working together to deliver non-blocking I/O at scale. Most developers use Node.js daily without understanding what actually happens when they call fs.readFile() or spin up an HTTP server. This post tears apart the internals so you can write faster, more reliable production code.

V8 Engine Internals: How JavaScript Runs

V8 is Google's open-source JavaScript engine written in C++. It compiles JavaScript directly to native machine code instead of interpreting it. Understanding V8's internals helps you write code that V8 can optimize effectively.

JIT Compilation Pipeline

V8 uses a multi-tier compilation strategy. When your code first runs, V8's interpreter called Ignition generates bytecode quickly. As functions get called repeatedly, V8's optimizing compiler called TurboFan kicks in and produces highly optimized machine code.

// This function will be optimized by TurboFan after repeated calls
function calculateSum(arr) {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    sum += arr[i]; // V8 assumes arr[i] is always a number
  }
  return sum;
}

// Calling with consistent types helps V8 optimize
for (let i = 0; i < 10000; i++) {
  calculateSum([1, 2, 3, 4, 5]); // Always numbers — V8 loves this
}

// This causes DEOPTIMIZATION — mixing types breaks assumptions
calculateSum([1, "two", 3]); // String in a number array — V8 bails out

Hidden Classes and Inline Caching

V8 creates internal "hidden classes" (also called Maps or Shapes) to track object structure. When you create an object and add properties in a consistent order, V8 assigns a hidden class and can use inline caching to speed up property access.

// GOOD: Consistent property order — same hidden class
function createUser(name, age) {
  const user = {};
  user.name = name; // Hidden class transition: HC0 -> HC1
  user.age = age;   // Hidden class transition: HC1 -> HC2
  return user;
}

const user1 = createUser("Alice", 30);
const user2 = createUser("Bob", 25);
// Both share the same hidden class chain — V8 can use inline caching

// BAD: Inconsistent property order — different hidden classes
function createUserBad(name, age, hasEmail) {
  const user = {};
  user.name = name;
  if (hasEmail) {
    user.email = "default@example.com"; // Conditional property changes hidden class
  }
  user.age = age;
  return user;
}
// user objects now have DIFFERENT hidden classes — inline caching fails

The takeaway: always initialize object properties in the same order, avoid adding properties conditionally, and never use delete on object properties in hot code paths — it destroys hidden class optimizations.

libuv and the Thread Pool

Node.js itself is single-threaded for JavaScript execution, but libuv — the C library that handles I/O — maintains a thread pool for operations that cannot be done asynchronously at the OS level. By default, this pool has 4 threads.

What Uses the Thread Pool?

Not all async operations use the thread pool. Here is what does and what does not:

Uses Thread PoolUses OS Async (epoll/kqueue/IOCP)
fs.readFile, fs.writeFileNetwork I/O (TCP, UDP, HTTP)
crypto.pbkdf2, crypto.randomBytesDNS resolution (dns.resolve)
zlib compressionTimers (setTimeout, setInterval)
dns.lookup (uses getaddrinfo)Signals
Some custom C++ addonsChild process events

Tuning UV_THREADPOOL_SIZE

If your application does heavy file I/O or crypto operations, the default 4 threads can become a bottleneck. You can increase it up to 1024.

# Set before starting your Node.js application
export UV_THREADPOOL_SIZE=16
node server.js

# On Windows PowerShell
$env:UV_THREADPOOL_SIZE = 16
node server.js
// You can also set it programmatically — but it MUST be before any I/O
process.env.UV_THREADPOOL_SIZE = "16";

// Demonstrate thread pool saturation
const crypto = require("crypto");
const start = Date.now();

for (let i = 0; i < 8; i++) {
  crypto.pbkdf2("password", "salt", 100000, 64, "sha512", () => {
    console.log(`Hash ${i + 1} done in ${Date.now() - start}ms`);
  });
}
// With UV_THREADPOOL_SIZE=4: first 4 finish together, next 4 wait
// With UV_THREADPOOL_SIZE=8: all 8 finish at roughly the same time

Event Loop Phases: The Complete Deep Dive

The event loop is not a simple queue. It has six distinct phases, each with its own FIFO queue of callbacks. Understanding these phases is essential for predicting execution order in complex applications.

Phase 1: Timers

Executes callbacks scheduled by setTimeout() and setInterval(). A timer does not guarantee exact execution at the specified time — it guarantees a minimum delay. The callback runs as soon as the event loop reaches this phase after the timer threshold has passed.

Phase 2: Pending Callbacks

Executes I/O callbacks deferred from the previous loop iteration. This includes some system-level callbacks like TCP connection errors (ECONNREFUSED).

Phase 3: Idle / Prepare

Used internally by Node.js only. You cannot schedule callbacks here directly.

Phase 4: Poll

This is where the event loop spends most of its time. It retrieves new I/O events, executes I/O-related callbacks (except close callbacks, timers, and setImmediate). If there are no timers scheduled, the poll phase will block and wait for new events.

Phase 5: Check

Executes setImmediate() callbacks. This phase runs immediately after the poll phase completes.

Phase 6: Close Callbacks

Handles close events, such as socket.on('close', ...).

// Demonstrating event loop phase order
const fs = require("fs");

// Timers phase
setTimeout(() => console.log("1: setTimeout"), 0);

// Check phase
setImmediate(() => console.log("2: setImmediate"));

// Reading a file triggers poll phase callback
fs.readFile(__filename, () => {
  // Inside an I/O callback, setImmediate ALWAYS runs before setTimeout
  setTimeout(() => console.log("3: setTimeout inside I/O"), 0);
  setImmediate(() => console.log("4: setImmediate inside I/O"));
});

// process.nextTick runs BETWEEN phases (microtask)
process.nextTick(() => console.log("5: nextTick"));

// Promise microtask — runs after nextTick, before next phase
Promise.resolve().then(() => console.log("6: Promise.then"));

console.log("7: Synchronous");

// Output:
// 7: Synchronous
// 5: nextTick
// 6: Promise.then
// 1: setTimeout (or 2 — order not guaranteed at top level)
// 2: setImmediate (or 1)
// 4: setImmediate inside I/O (always before 3 inside I/O callbacks)
// 3: setTimeout inside I/O

process.nextTick vs setImmediate vs setTimeout(0)

This is one of the most confusing aspects of Node.js. Here is the definitive comparison:

MethodWhen It RunsQueue Type
process.nextTick()After current operation, before event loop continuesMicrotask (nextTick queue)
Promise.then()After nextTick queue is drainedMicrotask (promise queue)
setTimeout(fn, 0)Timers phase of next event loop iterationMacrotask
setImmediate(fn)Check phase of current or next iterationMacrotask
// WARNING: Recursive nextTick can STARVE the event loop
// This is a real production bug pattern
function dangerousRecursion() {
  process.nextTick(() => {
    // This runs before ANY I/O callback — starves the event loop
    dangerousRecursion();
  });
}

// SAFE alternative: use setImmediate for recursive patterns
function safeRecursion() {
  setImmediate(() => {
    // This allows I/O callbacks to run between iterations
    safeRecursion();
  });
}

Blocking the Event Loop: Detection and Prevention

The single biggest performance killer in Node.js applications is blocking the event loop. Here are real examples of code that blocks, and how to detect it.

Common Blocking Patterns

// BLOCKING: Synchronous file read in a request handler
const http = require("http");
const fs = require("fs");

http.createServer((req, res) => {
  // This blocks ALL other requests while the file is being read
  const data = fs.readFileSync("/large-file.csv", "utf8");
  res.end(data);
}).listen(3000);

// BLOCKING: CPU-intensive computation
http.createServer((req, res) => {
  // Fibonacci calculation blocks for seconds
  function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
  const result = fibonacci(42); // Takes ~2 seconds — blocks everything
  res.end(`Result: ${result}`);
}).listen(3000);

// BLOCKING: Large JSON parsing
http.createServer((req, res) => {
  const hugeObject = JSON.parse(fs.readFileSync("500mb-data.json"));
  res.end("Done");
}).listen(3000);

Detecting Event Loop Blocking with --prof

# Profile your application
node --prof server.js

# Generate a load (use autocannon or ab)
npx autocannon -c 100 -d 10 http://localhost:3000

# Process the profile output (creates a .log file)
node --prof-process isolate-0x*.log > processed-profile.txt

# Look for "ticks" in the output — high percentage in your code = blocking
cat processed-profile.txt | head -50
// Programmatic event loop lag detection
let lastCheck = Date.now();

setInterval(() => {
  const now = Date.now();
  const lag = now - lastCheck - 1000; // Expected interval is 1000ms
  if (lag > 50) {
    console.warn(`Event loop lag detected: ${lag}ms`);
  }
  lastCheck = now;
}, 1000);

// Using the built-in perf_hooks module (Node.js 12+)
const { monitorEventLoopDelay } = require("perf_hooks");

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

setInterval(() => {
  console.log(`Event loop delay (p99): ${histogram.percentile(99) / 1e6}ms`);
  console.log(`Event loop delay (max): ${histogram.max / 1e6}ms`);
  histogram.reset();
}, 5000);

Streams Deep Dive

Streams are one of Node.js's most powerful features. They let you process data piece by piece instead of loading everything into memory. There are four types: Readable, Writable, Transform, and Duplex.

Readable Streams

const fs = require("fs");

// Reading a 2GB file — uses only ~64KB of memory at a time
const readable = fs.createReadStream("huge-file.csv", {
  encoding: "utf8",
  highWaterMark: 64 * 1024 // 64KB chunks
});

let lineCount = 0;

readable.on("data", (chunk) => {
  lineCount += chunk.split("
").length - 1;
});

readable.on("end", () => {
  console.log(`Total lines: ${lineCount}`);
});

readable.on("error", (err) => {
  console.error("Read error:", err.message);
});

Transform Streams: Real CSV Processing Example

const { Transform } = require("stream");
const fs = require("fs");

// Transform stream that converts CSV to JSON
class CsvToJson extends Transform {
  constructor(options) {
    super({ ...options, objectMode: true });
    this.headers = null;
    this.buffer = "";
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString();
    const lines = this.buffer.split("
");
    this.buffer = lines.pop(); // Keep incomplete line in buffer

    for (const line of lines) {
      if (!line.trim()) continue;
      const values = line.split(",").map(v => v.trim());

      if (!this.headers) {
        this.headers = values;
        continue;
      }

      const obj = {};
      this.headers.forEach((header, i) => {
        obj[header] = values[i] || "";
      });
      this.push(JSON.stringify(obj) + "
");
    }
    callback();
  }

  _flush(callback) {
    if (this.buffer.trim() && this.headers) {
      const values = this.buffer.split(",").map(v => v.trim());
      const obj = {};
      this.headers.forEach((header, i) => {
        obj[header] = values[i] || "";
      });
      this.push(JSON.stringify(obj) + "
");
    }
    callback();
  }
}

// Pipeline — proper error handling with stream.pipeline
const { pipeline } = require("stream");

pipeline(
  fs.createReadStream("users.csv"),
  new CsvToJson(),
  fs.createWriteStream("users.jsonl"),
  (err) => {
    if (err) {
      console.error("Pipeline failed:", err.message);
    } else {
      console.log("CSV to JSON conversion complete");
    }
  }
);

Writable Streams with Backpressure

const fs = require("fs");

const writable = fs.createWriteStream("output.txt");

function writeData(iterations) {
  let i = 0;

  function write() {
    let ok = true;
    while (i < iterations && ok) {
      const data = `Line ${i}: ${"-".repeat(100)}
`;
      i++;
      if (i === iterations) {
        writable.write(data, () => {
          console.log("All data written");
        });
      } else {
        // write() returns false when internal buffer is full
        ok = writable.write(data);
      }
    }
    if (i < iterations) {
      // Backpressure: wait for drain event before writing more
      writable.once("drain", write);
    }
  }

  write();
}

writeData(1000000); // Write 1 million lines without running out of memory

Worker Threads: True Parallelism in Node.js

Worker threads let you run JavaScript in parallel threads, sharing memory via SharedArrayBuffer. Unlike the cluster module, workers share the same process and can transfer data efficiently.

Basic Worker Thread Setup

// main.js
const { Worker, isMainThread, parentPort, workerData } = require("worker_threads");

if (isMainThread) {
  console.log("Main thread starting workers...");

  const worker = new Worker(__filename, {
    workerData: { start: 1, end: 1000000000 }
  });

  worker.on("message", (result) => {
    console.log(`Worker result: ${result}`);
  });

  worker.on("error", (err) => {
    console.error("Worker error:", err);
  });

  worker.on("exit", (code) => {
    if (code !== 0) {
      console.error(`Worker exited with code ${code}`);
    }
  });
} else {
  // This runs inside the worker thread
  const { start, end } = workerData;
  let sum = 0;
  for (let i = start; i <= end; i++) {
    sum += i;
  }
  parentPort.postMessage(sum);
}

SharedArrayBuffer and Atomics

// shared-counter.js — Multiple workers incrementing a shared counter
const { Worker, isMainThread } = require("worker_threads");

if (isMainThread) {
  const sharedBuffer = new SharedArrayBuffer(4); // 4 bytes = 1 Int32
  const sharedArray = new Int32Array(sharedBuffer);
  sharedArray[0] = 0;

  const NUM_WORKERS = 4;
  const INCREMENTS_PER_WORKER = 100000;
  let completedWorkers = 0;

  for (let i = 0; i < NUM_WORKERS; i++) {
    const worker = new Worker(__filename, {
      workerData: { sharedBuffer, increments: INCREMENTS_PER_WORKER }
    });

    worker.on("exit", () => {
      completedWorkers++;
      if (completedWorkers === NUM_WORKERS) {
        console.log(`Final counter value: ${sharedArray[0]}`);
        console.log(`Expected: ${NUM_WORKERS * INCREMENTS_PER_WORKER}`);
      }
    });
  }
} else {
  const { workerData } = require("worker_threads");
  const sharedArray = new Int32Array(workerData.sharedBuffer);

  for (let i = 0; i < workerData.increments; i++) {
    // Atomics.add ensures thread-safe increment
    Atomics.add(sharedArray, 0, 1);
  }
}

Worker Pool Pattern for Production

// worker-pool.js
const { Worker } = require("worker_threads");
const os = require("os");

class WorkerPool {
  constructor(workerFile, poolSize = os.cpus().length) {
    this.workerFile = workerFile;
    this.poolSize = poolSize;
    this.workers = [];
    this.freeWorkers = [];
    this.taskQueue = [];

    for (let i = 0; i < poolSize; i++) {
      this._addNewWorker();
    }
  }

  _addNewWorker() {
    const worker = new Worker(this.workerFile);

    worker.on("message", (result) => {
      worker._currentResolve(result);
      worker._currentResolve = null;
      this.freeWorkers.push(worker);
      this._processQueue();
    });

    worker.on("error", (err) => {
      if (worker._currentReject) {
        worker._currentReject(err);
      }
      this.workers.splice(this.workers.indexOf(worker), 1);
      this._addNewWorker();
    });

    this.workers.push(worker);
    this.freeWorkers.push(worker);
    this._processQueue();
  }

  _processQueue() {
    if (this.taskQueue.length === 0 || this.freeWorkers.length === 0) return;
    const { data, resolve, reject } = this.taskQueue.shift();
    const worker = this.freeWorkers.pop();
    worker._currentResolve = resolve;
    worker._currentReject = reject;
    worker.postMessage(data);
  }

  runTask(data) {
    return new Promise((resolve, reject) => {
      this.taskQueue.push({ data, resolve, reject });
      this._processQueue();
    });
  }

  destroy() {
    for (const worker of this.workers) {
      worker.terminate();
    }
  }
}

module.exports = WorkerPool;

// Usage:
// const pool = new WorkerPool("./heavy-task-worker.js", 4);
// const result = await pool.runTask({ input: "some data" });

Cluster Module for Multi-Core Scaling

While worker threads share memory within a process, the cluster module forks entirely separate processes to utilize all CPU cores.

const cluster = require("cluster");
const http = require("http");
const os = require("os");

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  console.log(`Primary process ${process.pid} starting ${numCPUs} workers`);

  // Fork workers
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // Graceful restart on worker crash
  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`);
    setTimeout(() => cluster.fork(), 1000); // Delay to prevent restart loops
  });

  // IPC: Send messages to workers
  for (const id in cluster.workers) {
    cluster.workers[id].send({ type: "config", data: { maxConn: 100 } });
  }

  // Graceful shutdown
  process.on("SIGTERM", () => {
    console.log("Primary received SIGTERM. Shutting down workers...");
    for (const id in cluster.workers) {
      cluster.workers[id].send({ type: "shutdown" });
    }
    setTimeout(() => process.exit(0), 5000);
  });
} else {
  const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Handled by worker ${process.pid}
`);
  });

  server.listen(3000);
  console.log(`Worker ${process.pid} started`);

  // Handle IPC messages from primary
  process.on("message", (msg) => {
    if (msg.type === "shutdown") {
      console.log(`Worker ${process.pid} shutting down gracefully`);
      server.close(() => process.exit(0));
    }
  });
}

child_process: exec, spawn, and fork

const { exec, spawn, fork } = require("child_process");

// exec: Buffers output, good for short commands
exec("git log --oneline -10", (err, stdout, stderr) => {
  if (err) {
    console.error("exec error:", err.message);
    return;
  }
  console.log("Recent commits:
", stdout);
});

// spawn: Streams output, good for long-running or large output
const ls = spawn("ls", ["-la", "/var/log"], { cwd: "/" });

ls.stdout.on("data", (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on("data", (data) => {
  console.error(`stderr: ${data}`);
});

ls.on("close", (code) => {
  console.log(`ls exited with code ${code}`);
});

// fork: Spawns a new Node.js process with IPC channel
const child = fork("./background-job.js");

child.send({ task: "processImages", dir: "/uploads" });

child.on("message", (result) => {
  console.log("Child completed:", result);
});

Memory Management and Leak Detection

V8 Heap Configuration

# Increase old-generation heap size (default ~1.5GB on 64-bit)
node --max-old-space-size=4096 server.js

# View current memory usage
node -e "console.log(process.memoryUsage())"
# Output: { rss, heapTotal, heapUsed, external, arrayBuffers }

Detecting Memory Leaks

// Common memory leak: growing arrays or maps
const leakyCache = {};

function handleRequest(userId) {
  // This cache grows forever — classic leak
  if (!leakyCache[userId]) {
    leakyCache[userId] = {
      data: Buffer.alloc(1024), // 1KB per user
      timestamp: Date.now()
    };
  }
  return leakyCache[userId];
}

// FIX: Use an LRU cache with a maximum size
const LRU = require("lru-cache");
const cache = new LRU({ max: 500 }); // Max 500 entries

function handleRequestFixed(userId) {
  let userData = cache.get(userId);
  if (!userData) {
    userData = { data: Buffer.alloc(1024), timestamp: Date.now() };
    cache.set(userId, userData);
  }
  return userData;
}

// Monitoring memory in production
setInterval(() => {
  const usage = process.memoryUsage();
  console.log({
    rss: `${Math.round(usage.rss / 1024 / 1024)}MB`,
    heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
    external: `${Math.round(usage.external / 1024 / 1024)}MB`
  });
}, 10000);

Heap Snapshot Analysis

# Install heapdump
npm install heapdump

# Take a snapshot programmatically
node -e "
const heapdump = require('heapdump');
heapdump.writeSnapshot('./heap-' + Date.now() + '.heapsnapshot');
"

# Or use --inspect and Chrome DevTools
node --inspect server.js
# Open chrome://inspect in Chrome, click the Node.js target
# Go to Memory tab -> Take heap snapshot

Performance Hooks and Measurement

const { performance, PerformanceObserver } = require("perf_hooks");

// Measure function execution time
performance.mark("start-db-query");

async function fetchUsers() {
  // Simulated database query
  return new Promise(resolve => setTimeout(resolve, 150));
}

await fetchUsers();

performance.mark("end-db-query");
performance.measure("DB Query", "start-db-query", "end-db-query");

// Observer to collect measurements
const obs = new PerformanceObserver((items) => {
  for (const entry of items.getEntries()) {
    console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
  }
});
obs.observe({ entryTypes: ["measure"] });

Building a Real-Time Chat Server with WebSockets

// chat-server.js
const http = require("http");
const { WebSocketServer } = require("ws");

const server = http.createServer();
const wss = new WebSocketServer({ server });

const rooms = new Map();

wss.on("connection", (ws) => {
  let currentRoom = null;
  let username = null;

  ws.on("message", (raw) => {
    let msg;
    try {
      msg = JSON.parse(raw.toString());
    } catch {
      ws.send(JSON.stringify({ error: "Invalid JSON" }));
      return;
    }

    switch (msg.type) {
      case "join":
        username = msg.username;
        currentRoom = msg.room || "general";

        if (!rooms.has(currentRoom)) {
          rooms.set(currentRoom, new Set());
        }
        rooms.get(currentRoom).add(ws);

        broadcast(currentRoom, {
          type: "system",
          text: `${username} joined the room`,
          timestamp: Date.now()
        }, ws);
        break;

      case "message":
        if (!currentRoom || !username) {
          ws.send(JSON.stringify({ error: "Join a room first" }));
          return;
        }
        broadcast(currentRoom, {
          type: "message",
          username,
          text: msg.text,
          timestamp: Date.now()
        });
        break;

      case "typing":
        broadcast(currentRoom, {
          type: "typing",
          username
        }, ws);
        break;
    }
  });

  ws.on("close", () => {
    if (currentRoom && rooms.has(currentRoom)) {
      rooms.get(currentRoom).delete(ws);
      broadcast(currentRoom, {
        type: "system",
        text: `${username} left the room`,
        timestamp: Date.now()
      });
      if (rooms.get(currentRoom).size === 0) {
        rooms.delete(currentRoom);
      }
    }
  });
});

function broadcast(room, data, exclude = null) {
  const clients = rooms.get(room);
  if (!clients) return;
  const payload = JSON.stringify(data);
  for (const client of clients) {
    if (client !== exclude && client.readyState === 1) {
      client.send(payload);
    }
  }
}

server.listen(3000, () => {
  console.log("Chat server running on ws://localhost:3000");
});
# Install the ws package
npm install ws

# Start the server
node chat-server.js

# Quick test with wscat
npx wscat -c ws://localhost:3000
> {"type":"join","username":"alice","room":"general"}
> {"type":"message","text":"Hello everyone!"}

Common Node.js Production Issues and Fixes

Problem: MaxListenersExceededWarning

Cause: Adding more than 10 listeners to an EventEmitter, usually from repeatedly attaching listeners inside a loop or request handler without removing them.

// BAD: Adding a listener on every request
const EventEmitter = require("events");
const emitter = new EventEmitter();

app.get("/data", (req, res) => {
  emitter.on("update", (data) => {  // Listener added on EVERY request — leak!
    res.json(data);
  });
});

// FIX: Use once() or remove listeners properly
app.get("/data", (req, res) => {
  const handler = (data) => {
    res.json(data);
  };
  emitter.once("update", handler); // Automatically removed after one call

  req.on("close", () => {
    emitter.removeListener("update", handler); // Clean up if client disconnects
  });
});

// If you legitimately need more listeners:
emitter.setMaxListeners(50); // Increase the limit

Problem: Unhandled Promise Rejections

Cause: An async function throws an error and there is no .catch() or try/catch to handle it. Starting in Node.js 15+, this crashes the process.

// BAD: No error handling
async function fetchData() {
  const response = await fetch("https://api.example.com/data");
  return response.json(); // Throws if API is down
}
fetchData(); // No .catch() — unhandled rejection!

// FIX: Always handle rejections
fetchData().catch(err => {
  console.error("Failed to fetch data:", err.message);
});

// Global safety net (log, do NOT ignore)
process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
  // In production: send to error tracking service, then exit
  process.exit(1);
});

Problem: EMFILE — Too Many Open Files

Cause: Opening too many files or sockets simultaneously, exceeding the OS file descriptor limit.

# Check current limit
ulimit -n

# Increase it temporarily
ulimit -n 65536

# Permanent fix on Linux: edit /etc/security/limits.conf
# * soft nofile 65536
# * hard nofile 65536
// Use graceful-fs as a drop-in replacement
const fs = require("graceful-fs"); // npm install graceful-fs
// It queues open() calls and retries on EMFILE errors

Troubleshooting Reference

ProblemCauseSolution
High event loop lagSynchronous operations or CPU-heavy code on main threadMove to Worker Threads or child_process
Memory keeps growingUncollected references in closures, caches, or event listenersUse heap snapshots, LRU caches, and remove listeners
ECONNRESET errorsRemote server closed connection unexpectedlyAdd retry logic with exponential backoff
Server handles fewer requests over timeThread pool exhaustion from too many fs/crypto callsIncrease UV_THREADPOOL_SIZE, batch operations
ENOMEM during buildV8 heap limit reachedUse --max-old-space-size=4096 flag
Port already in use (EADDRINUSE)Another process occupies the portlsof -i :3000 to find PID, then kill it

Quick Reference Cheat Sheet

# V8 Flags
node --max-old-space-size=4096 app.js    # Increase heap to 4GB
node --prof app.js                        # CPU profiling
node --inspect app.js                     # Debug with Chrome DevTools
node --inspect-brk app.js                 # Break on first line
node --trace-warnings app.js              # Show full warning stack traces

# Environment Variables
UV_THREADPOOL_SIZE=16 node app.js         # Increase libuv thread pool
NODE_OPTIONS="--max-old-space-size=4096"  # Set V8 options via env

# Debugging
node -e "console.log(process.versions)"   # Show V8/Node versions
node -e "console.log(process.memoryUsage())" # Memory snapshot
node -e "console.log(os.cpus().length)"   # CPU count

# Process Monitoring
npx clinic doctor -- node server.js       # Diagnose performance issues
npx autocannon http://localhost:3000      # Load testing
npx 0x server.js                          # Flame graph profiling
// Quick Reference: Event Loop Order
// 1. Synchronous code
// 2. process.nextTick callbacks
// 3. Promise microtasks (.then, .catch, .finally)
// 4. Timers (setTimeout, setInterval)
// 5. I/O callbacks
// 6. setImmediate callbacks
// 7. Close callbacks

// Quick Reference: Stream Pipeline (Node 15+)
const { pipeline } = require("stream/promises");

await pipeline(
  fs.createReadStream("input.txt"),
  new Transform({ transform(chunk, enc, cb) { cb(null, chunk.toString().toUpperCase()); } }),
  fs.createWriteStream("output.txt")
);

// Quick Reference: Worker Thread Communication
// Main -> Worker: worker.postMessage(data)
// Worker -> Main: parentPort.postMessage(data)
// Shared Memory: SharedArrayBuffer + Atomics

Node.js's power lies in understanding its internals. By mastering the event loop, streams, worker threads, and memory management, you build applications that are not just functional but genuinely production-ready. At DreamWebCrafts, we architect Node.js backends that handle real-world scale — from WebSocket-powered real-time dashboards to high-throughput API servers. If your project demands performance that goes beyond the basics, our team is ready to help you build it right.