Back to blogs
2025-08-22
2 min read
How Node.js Actually Works
Explore Node.js internals: V8, libuv, the event loop phases, thread pool, microtasks vs macrotasks, and practical patterns for performance and reliability.
How Node.js Actually Works
Node.js delivers high concurrency with a single JS thread by combining V8 for execution and libuv for asynchronous I/O.
Key Components
V8 JavaScript Engine
- JIT compiles JS to machine code for speed
- Modern GC strategies; fast property access and inline caches
libuv
- Cross‑platform async I/O library
- Provides the event loop + thread pool (4 by default, configurable)
Event Loop Phases (High‑Level)
┌──────── timers (setTimeout / setInterval)
├──────── pending callbacks (deferred I/O)
├──────── idle/prepare (internal)
├──────── poll (I/O events, execute callbacks)
├──────── check (setImmediate)
└──────── close callbacks (e.g., socket 'close')
Two distinct queues also matter:
- Macrotasks: timers, I/O callbacks
- Microtasks:
Promise.then
,queueMicrotask
(run between macrotasks)
Microtasks vs Macrotasks
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("microtask"));
// Output: microtask -> timeout
Thread Pool (File I/O, Crypto, DNS)
Some operations use libuv’s thread pool to avoid blocking the main thread.
const fs = require("fs");
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) throw err;
console.log(data);
});
Increase pool size (if many concurrent I/O tasks):
UV_THREADPOOL_SIZE=8 node server.js
Practical Patterns
Offload CPU‑Bound Tasks
// worker_threads (for heavy CPU work)
const { Worker } = require("node:worker_threads");
new Worker("./heavy-task.js", { workerData: { n: 5_000_000 } });
Avoid Blocking Calls
- Don’t use sync FS/crypto on the hot path
- Stream large payloads; backpressure with
.pipe()
Schedule Wisely
// Use setImmediate for post‑I/O continuation
req.on("end", () => {
setImmediate(() => handleRequest(req, res));
});
Measure and Observe
// async_hooks or perf_hooks to trace async resources
const asyncHooks = require("async_hooks");
asyncHooks
.createHook({
init: (id, type, trigger) => {
/* record */
},
destroy: (id) => {
/* cleanup */
},
})
.enable();
Common Pitfalls
- Long synchronous loops block everything (no rendering, no I/O)
- Burst of microtasks can starve timers
- Large JSON.stringify/parse on hot paths
Summary
- V8 executes JS fast; libuv orchestrates async I/O
- Event loop phases and microtasks/macrotasks ordering shape behavior
- Offload CPU work, stream I/O, and monitor async activity for performance