Skip to main content

Why and When You Need This

The other guides in this section explain how Node.js works internally — the event loop, module system, streams, and so on, traced down to the C++ and libuv layer. This page is the other half: for each of those subsystems, why it's worth knowing, where it actually shows up in real projects, and the moment it stops being trivia and starts being the thing standing between you and a fix.

Each entry also links to the relevant internal guide and, where one exists, the canonical module page on the official Node.js API reference — these guides explain the mechanism; the official docs remain the source of truth for the public API surface and version-specific behavior.


Async Internals — the event loop, AsyncHook, AsyncLocalStorage​

Why it matters: This is the mechanism underneath every other async concept on this page. Once you know how nextTick, microtasks, and the async ID system actually work, "callback never fires" and "my context disappeared halfway through a request" stop being mysteries.

Where it's used: APM and logging libraries use AsyncHook to trace work across callbacks; web frameworks use AsyncLocalStorage to carry a request ID or tenant ID through a call chain without passing it as an argument everywhere.

When you'll need it: Debugging why a value set earlier in a request (like a request ID in a logger) is undefined by the time an await resolves, or building any tool that needs to track one logical operation across several async hops.

Guide: Async Internals · Reference: async_hooks, AsyncLocalStorage


Module Loading — CommonJS vs ESM​

Why it matters: require() and import are two separate loaders with separate caches and separate rules for circular dependencies. Treating them as interchangeable is where "works locally, breaks in CI" bugs come from.

Where it's used: Every Node codebase, but it matters most when you're publishing a package that needs to work for both CJS and ESM consumers, or maintaining a monorepo that mixes both.

When you'll need it: Hitting Cannot use import statement outside a module, debugging a circular require() that returns a half-built object, or deciding how to shape a package's exports field.

Guide: Module Loading · Reference: CommonJS modules, ECMAScript modules


Performance — the internals that set your ceiling​

Why it matters: This guide ties the event loop, V8's JIT tiers, the buffer pool, stream backpressure, and the thread pool together into one picture — the handful of mechanisms that actually determine throughput and latency, rather than guesses.

Where it's used: Performance tuning sessions, capacity planning before a launch, and incident reviews after a latency spike.

When you'll need it: Your service passes every functional test but falls over under real load, or someone asks "why did p99 latency jump" and "add more replicas" isn't a real answer yet.

Guide: Performance · Reference: Performance measurement APIs, V8


File System (fs) — what readFile is hiding​

Why it matters: fs calls look synchronous in shape but run on a 4-thread pool underneath. That gap explains file-descriptor exhaustion, why fs.watch behaves differently on Linux/macOS/Windows, and why "just read the whole file" stops working once files get large.

Where it's used: File upload and processing services, log rotation, config hot-reload, CLI tools that read project files.

When you'll need it: You hit EMFILE: too many open files, you're deciding between fs.readFile and fs.createReadStream for a multi-gigabyte file, or you're writing a file watcher that needs to behave the same across operating systems.

Guide: File System · Reference: fs


Streams — backpressure, not just data in motion​

Why it matters: Backpressure is the one mechanism standing between "pipe a 10GB upload through your server" and an out-of-memory crash. Streams are also the shared primitive underneath fs, http, and zlib, so understanding them once pays off everywhere.

Where it's used: Upload/download pipelines, proxying HTTP responses, compressing large payloads, any ETL-style data pipeline.

When you'll need it: Memory climbs steadily while data is flowing through your process, you're ignoring write()'s false return value, or you're building a transform pipeline instead of buffering everything in an array first.

Guide: Streams · Reference: stream


Event Loop — the ordering model for everything async​

Why it matters: It's the single mental model that explains the order timers, I/O callbacks, and microtasks actually run in. Most "this ran in the wrong order" bugs trace back to a gap in this model, not a bug in your code.

Where it's used: Everywhere — middleware chains, request handlers, background jobs, anywhere more than one async operation is in flight at once.

When you'll need it: A setImmediate fires somewhere you didn't expect, or a synchronous block (a big JSON.parse, a tight loop) freezes every other request your server is handling, not just the one that triggered it.

Guide: Event Loop · Reference: timers


Async I/O — what's actually non-blocking​

Why it matters: "Node is non-blocking" is true for sockets and pipes, and quietly false for fs, dns.lookup(), crypto, and zlib — those run on a 4-thread pool by default. That gap between the marketing and the mechanism is exactly where throughput plateaus sneak up on you.

Where it's used: Any service doing file I/O, password hashing, or DNS-heavy outbound calls — anywhere work quietly depends on that shared thread pool.

When you'll need it: Throughput flatlines around four concurrent operations no matter how much "everything is async," or you're deciding whether UV_THREADPOOL_SIZE needs tuning.

Guide: Async I/O · Reference: fs, dns


Promises and Async/Await — today's default async style​

Why it matters: Nearly all modern Node code is written this way, so microtask timing and rejection handling aren't optional knowledge — they're the baseline for reading any codebase.

Where it's used: API handlers, retry/backoff logic, fan-out calls with Promise.all, basically every file in a modern codebase.

When you'll need it: An unhandled rejection crashes the process, Promise.all fails fast when you actually needed Promise.allSettled, or two awaits "interleave" in a way that doesn't match the order they're written in.

Guide: Promises and Async/Await · Reference: process — unhandledRejection


Concurrency — worker threads, cluster, and child processes​

Why it matters: Node's JavaScript is single-threaded, but your server isn't running on one core. Worker threads, cluster, and child processes are three different escape hatches for three different problems, and picking the wrong one wastes the hardware you're paying for.

Where it's used: CPU-heavy work (image or PDF processing, large data transforms) running inside an API server, scaling a web server across every core in a container, isolating a flaky dependency in its own process.

When you'll need it: A CPU-bound task blocks every other request while it runs, or your container has 4 vCPUs but your Node process only ever shows load on one of them.

Guide: Concurrency · Reference: worker_threads, cluster, child_process


Memory Management and GC — what V8 won't tell you​

Why it matters: Garbage collection is automatic, not magic. Closures that outlive their usefulness, unbounded in-process caches, and Buffer's off-heap memory all cause real production memory growth that node --max-old-space-size alone won't fix.

Where it's used: Long-running services and daemons, anything that caches in-process, WebSocket servers holding many open connections.

When you'll need it: Memory climbs until the container gets OOM-killed, you're tuning heap size flags, or you're chasing a leak with heap snapshots instead of guessing.

Guide: Memory Management · Reference: v8, buffer


Production Scaling — CPU-bound vs I/O-bound​

Why it matters: Whether a workload is CPU-bound or I/O-bound decides whether the fix is more replicas, more cores, or just better async code — and getting it backwards means paying for hardware that doesn't help.

Where it's used: Capacity planning, writing the Dockerfile or Kubernetes deployment that decides replica count and resource limits.

When you'll need it: Deciding how many replicas vs how many worker threads to run, or noticing that horizontal scaling has stopped reducing latency past a certain point.

Guide: Production Scaling · Reference: cluster


HTTP Request Lifecycle — from TCP accept to response​

Why it matters: This is what's actually happening between a client connecting and your handler firing — the accept queue, the parser, keep-alive — which is exactly where load-balancer and timeout mysteries live.

Where it's used: Building or debugging an HTTP server, API gateway, or reverse proxy, tuning keep-alive behavior behind a load balancer.

When you'll need it: A load balancer reports sporadic 502s, you're choosing a keepAliveTimeout value, or a response seems to "hang" even though your handler already returned.

Guide: HTTP Request Lifecycle · Reference: http


Quick index​

If you're facing this...Read this guide
A value set earlier in a request disappears after an awaitAsync Internals
Cannot use import statement outside a moduleModule Loading
p99 latency jumped and "add replicas" isn't an answer yetPerformance
EMFILE: too many open filesFile System
Memory grows steadily while streaming data throughStreams
A setImmediate or sync block runs at the "wrong" timeEvent Loop
Throughput plateaus around 4 concurrent operationsAsync I/O
An unhandled rejection crashed the processPromises and Async/Await
One CPU-bound task blocks every other requestConcurrency
The container got OOM-killedMemory Management
Deciding replica count vs worker threadsProduction Scaling
Sporadic 502s from a load balancerHTTP Request Lifecycle