Node.js Fundamentals - Runtime and Non-Blocking I/O

Source: The NodeBook - What Node.js Is Interesting read - Ryan Dahl’s original presentation -

Excalidraw:

Transclude of Node.js-Fundamentals---Runtime-and-Non-Blocking-IO.excalidraw

One-line definition

Node.js is a JavaScript runtime for server-side programs, CLIs, build tools, and automation.

It runs JavaScript through V8, exposes system APIs through Node core, and uses libuv for the event loop, timers, operating-system I/O, and the worker pool behind file-system, dns.lookup(), crypto, and zlib work.

Runtime boundary

The most useful mental model is that Node is not just V8.

ComponentResponsibility
V8Parses, executes, optimizes JavaScript, and manages the JavaScript heap.
Node core APIsProvides JavaScript modules such as node:fs, node:http, node:net, node:crypto, node:stream, node:timers, and node:process.
C++ Native bindingsBridge JavaScript-facing APIs into C/C++ and platform-level capabilities like accessing file system (JS does not have any way to read/write files)
libuvProvides the event loop, timers, async I/O abstractions, child-process support, and the worker pool.
npm ecosystemUserland packages layered on top of the runtime.

V8 handles JavaScript execution. libuv tracks asynchronous work and reports that it’s ready. C++ bindings connect JavaScript API calls to native code, like accessing file system, as Javascript doesn’t have any way to read/write files. Node’s standard library then gives you modules such as node:fsnode:netnode:httpnode:crypto, and node:timers as ordinary JavaScript entry points into all that machinery.

Interview phrasing:

Node is a single-main-thread JavaScript runtime with native machinery around it. JavaScript callbacks do not run in parallel on the main thread, but the runtime can keep waiting work outside the JS stack and schedule continuations later.

Why Node was created

Around the C10k era, many servers used a thread-per-request or process-per-request model:

  1. A request arrives.
  2. The server assigns a thread or process.
  3. Application code runs there.
  4. If the request waits on a database, disk, network call, or another service, the thread sits blocked.
  5. The thread becomes available only after the wait finishes and the response is sent.

Ryan Dahl argued that this wasted resources because many web workloads are I/O-bound. The CPU is often idle while a request is waiting on external systems.

Node applied an event-driven, non-blocking model to server-side JavaScript:

  • Register interest in I/O.
  • Return to the event loop.
  • Keep the main JavaScript thread free.
  • Run the continuation only when the OS, timer, or worker pool reports completion.

The dominant pattern is to keep the main thread available while slow work proceeds elsewhere. That is why core APIs have asynchronous alternatives, why current Node.js code usually uses Promises and async/await, and why synchronous APIs such as fs.readFileSync are treated as deliberate blocking choices rather than the default for request paths.

V8 does not just interpret source line by line, instead it parses JavaScript to bytecode and may compile warm or hot paths to machine code through several Just-In-Time (JIT) tiers. Modern V8 is multi-tiered. V8 first compiles JavaScript to Ignition bytecode, which is executed by the Ignition interpreter. As code gets hotter, it may tier up through Sparkplug and/or Maglev, and sufficiently hot code can be optimized by TurboFan, which is V8’s top-tier optimizing compiler.

Blocking vs non-blocking

Blocking work holds the JavaScript call stack. While that stack is occupied, no other JavaScript callback in that process can run.

Examples:

  • CPU busy wait
  • Large synchronous computation
  • fs.readFileSync() on a request path
  • Synchronous crypto or compression

Non-blocking work moves the wait outside the JavaScript stack.

Examples:

  • Socket I/O waiting in the operating system
  • Timers waiting in libuv
  • Filesystem work dispatched through the libuv worker pool
  • crypto, zlib, and dns.lookup() work using native paths or the worker pool

Important nuance:

Non-blocking does not mean the callback runs immediately. It means the initiating JavaScript call can return before the external work finishes.

Request flow mental model

For an async operation such as fs.readFile, the simplified path is:

  1. Application JavaScript calls a Node core API.
  2. Node core enters internal native bindings.
  3. Native code creates one or more libuv requests.
  4. The JavaScript call returns before the native work is complete.
  5. libuv tracks the operation through the OS or worker pool.
  6. When the work completes, libuv reports back to the event loop.
  7. Node schedules the callback or Promise reaction.
  8. V8 executes the continuation on a later turn of the JavaScript thread.

The key phrase for interviews:

Waiting happens outside the JS stack; continuation happens back on the JS thread.

libuv and the worker pool

libuv gives Node a cross-platform event loop and async platform layer.

OS-level network readiness:

  • Linux: epoll
  • macOS/BSD: kqueue
  • Windows: IOCP

Most network socket I/O uses OS readiness mechanisms and does not consume libuv worker-pool threads.

The worker pool is used when the OS does not provide a clean non-blocking interface or when native work would otherwise block the event loop. Common examples:

  • Many filesystem APIs
  • dns.lookup()
  • Some crypto work
  • Some zlib work

Staff-level nuance:

Node is single-threaded from the JavaScript perspective, but not from the runtime perspective. The process uses additional native threads for specific categories of work.

Concurrency is not parallel JavaScript

Only one JavaScript callback runs on the main thread at a time.

Async callbacks interleave over time:

  • Request A starts I/O.
  • Request B starts while A is waiting.
  • B might complete before A.
  • The order of completions can change.

This avoids many traditional shared-memory locking problems, but it does not eliminate correctness issues.

You can still have:

  • Logical races
  • Lost updates
  • Out-of-order responses
  • Stale reads
  • Resource starvation
  • Event-loop delay from CPU-heavy work

Timer wait vs blocking wait

The article contrasts two servers:

  • A blocking server does busyWait(500) per request. Ten concurrent requests take roughly five seconds because each callback occupies the JS stack for 500ms.
  • A timer server uses setTimeout(..., 500). Ten concurrent requests finish in just over 500ms because each request schedules a timer and returns to the event loop.

The timer callbacks still execute one at a time. They just become eligible around the same time.

Interview phrasing:

setTimeout does not create parallel JavaScript execution. It registers a timer handle, frees the stack, and queues the callback after the timer becomes eligible.

Design implications

For production Node services:

  • Avoid synchronous APIs on request paths.
  • Avoid long CPU-bound work on the main event loop.
  • Use streams for large data instead of reading everything into memory.
  • Understand which operations use the libuv worker pool.
  • Be careful with UV_THREADPOOL_SIZE; increasing it can help contention but can also increase CPU scheduling and memory pressure.
  • Watch event-loop lag as a production signal.
  • Treat dependency management as production work because npm packages can affect build-time, startup-time, and request-path behavior.

Senior/staff interview checklist

Be ready to explain:

  • Why Node scales well for I/O-bound workloads.
  • Why Node is not ideal for CPU-heavy work unless you move that work to worker threads, child processes, native services, or external compute.
  • Difference between concurrency and parallelism.
  • Difference between OS-level non-blocking network I/O and libuv worker-pool offload.
  • What blocks the event loop.
  • Why fs.readFile() is async but still all-in-memory at the JavaScript API level.
  • Why streams are often better for large files or high-throughput data paths.
  • Why logical races can still happen even though JS callbacks do not run simultaneously on the main thread.

Compact answer

Node.js wraps V8 with Node core APIs, native bindings, and libuv. V8 executes JavaScript on the main thread, while libuv and the operating system track timers, sockets, and selected native work outside the JavaScript stack. When external work finishes, Node schedules a callback or Promise continuation to run later on the JavaScript thread. This makes Node efficient for I/O-bound concurrency, but it does not make JavaScript parallel by default. CPU-heavy work and synchronous APIs still block the event loop.