Node.js Interview Prep
Node.js Internals

What is Node.js?

Runtime, V8, and the Non-Blocking Revolution

LinkedIn Hook

"JavaScript is a browser language." — every developer before 2009.

That one sentence was a cage. For 14 years, JavaScript was trapped inside <script> tags, forbidden from touching the filesystem, forbidden from opening a socket, forbidden from being a real backend language. If you wanted to build a server, you reached for PHP, Python, Ruby, or Java — and you accepted that every incoming request would spawn a thread that mostly sat around waiting for the database.

Then in 2009, Ryan Dahl watched a progress bar in a browser and got angry. Apache was choking on 10,000 concurrent connections. Threads were expensive. Memory was bleeding. He asked a simple question: what if a server never blocked on I/O? What if one thread could juggle thousands of sockets by never waiting for any of them?

The answer was Node.js — Google's V8 engine bolted onto an async I/O library called libuv, wrapped in a JavaScript API that made non-blocking code feel natural. Today it powers Netflix, LinkedIn, PayPal, Uber, and half the npm-adjacent universe.

But here's what most developers get wrong in interviews: Node.js is not a language. Not a framework. Not multi-threaded. It's a runtime. And understanding the difference is the entire reason you should care about this lesson.

Read the full lesson -> [link]

#NodeJS #Backend #JavaScript #V8 #libuv #InterviewPrep


What is Node.js? thumbnail


What You'll Learn

  • The exact difference between a language, a runtime, and a framework (and why Node.js is only one of them)
  • How V8 and libuv combine to execute JavaScript outside the browser
  • Why Node.js is called "single-threaded but non-blocking" — and why that phrase is half-wrong
  • The origin story: why Ryan Dahl built Node.js and what problem it solved
  • When Node.js is the right choice (I/O-heavy APIs, realtime, streaming)
  • When Node.js is the wrong choice (CPU-heavy math, video encoding, image processing)
  • How Node.js differs from the JavaScript that runs in your browser

The Restaurant Analogy — One Waiter, A Hundred Tables

Imagine two restaurants on the same street.

Restaurant A (Apache / PHP model): Every customer gets their own dedicated waiter. When you sit down, a waiter walks over, takes your order, walks to the kitchen, stands there waiting for the chef to cook your food, carries it back, and then serves you. If 100 customers arrive, the restaurant hires 100 waiters. Most of them stand idle, staring at the kitchen. Payroll is enormous. The building runs out of floor space before it runs out of food.

Restaurant B (Node.js model): There is only one waiter. When you sit down, the waiter takes your order, hands a ticket to the kitchen, and immediately walks away to take the next table's order. When any kitchen ticket is ready, a bell rings, and the waiter swings by to deliver that plate. One waiter can serve hundreds of tables because taking orders is fast, but cooking is slow — and there's no reason for the waiter to stand still while the chef works.

That one waiter is the Node.js main thread. The kitchen is the operating system. The tickets are I/O operations (database queries, file reads, network calls). The bell is the event loop. The waiter never waits — they just register a callback and keep moving.

This is why Node.js can handle tens of thousands of concurrent connections on a single process, using a fraction of the memory that a thread-per-request server would need.

+---------------------------------------------------------------+
|           THREAD-PER-REQUEST (Apache, old PHP)                |
+---------------------------------------------------------------+
|                                                                |
|  Request 1 -> Thread 1 -> [BLOCKED waiting on DB] -> respond  |
|  Request 2 -> Thread 2 -> [BLOCKED waiting on DB] -> respond  |
|  Request 3 -> Thread 3 -> [BLOCKED waiting on DB] -> respond  |
|  ...                                                           |
|  Request 10000 -> Thread 10000 -> OUT OF MEMORY               |
|                                                                |
|  Each thread costs ~1-2 MB of stack. 10k threads = 10-20 GB.  |
|                                                                |
+---------------------------------------------------------------+

+---------------------------------------------------------------+
|           EVENT LOOP (Node.js)                                |
+---------------------------------------------------------------+
|                                                                |
|  Single thread: [req1 -> register DB callback] (non-blocking) |
|                 [req2 -> register DB callback] (non-blocking) |
|                 [req3 -> register DB callback] (non-blocking) |
|                 ...                                            |
|                 [DB done for req1 -> run callback -> respond] |
|                 [DB done for req3 -> run callback -> respond] |
|                                                                |
|  One thread. ~20 MB RAM. 10k+ concurrent connections easy.    |
|                                                                |
+---------------------------------------------------------------+

Napkin AI Visual Prompt: "Dark gradient (#0a1a0a -> #0d2e16). Split comparison: LEFT side labeled 'Thread-per-request' shows 100 waiter figures stacked in rows, each tied to a single table with red #ef4444 chains labeled 'BLOCKED'. RIGHT side labeled 'Node.js event loop' shows one green (#68a063) waiter figure in the center with glowing motion lines, surrounded by 100 tables all being served in parallel, amber (#ffb020) notification bells floating above. A timeline below shows: 'wait, wait, wait' (left) vs 'dispatch, dispatch, dispatch' (right). White monospace labels throughout."


Runtime vs Language vs Framework — The Question That Trips Everyone Up

This is the first thing an interviewer will probe, and the wrong answer instantly signals a junior. Let's nail it.

  • Language: A set of syntax rules and semantics. JavaScript is a language — defined by the ECMAScript specification (ECMA-262). The language itself knows nothing about files, networks, or timers. It knows about variables, functions, objects, and control flow. That's it.

  • Runtime: An environment that executes a language and provides it with capabilities the language alone doesn't have. A runtime includes a parser, a compiler or interpreter, a memory manager, and a set of host APIs (filesystem, network, process, timers). Node.js is a runtime. So is the browser. Both run JavaScript, but they expose different host APIs.

  • Framework: A library of opinionated code built on top of a runtime to solve a specific problem (web servers, UI, testing). Express, NestJS, Fastify, Koa are frameworks. They run inside the Node.js runtime. Node.js itself is not a framework — it ships no router, no template engine, no opinion about how you structure an app.

+---------------------------------------------------------------+
|           THE STACK                                           |
+---------------------------------------------------------------+
|                                                                |
|   [ Your App ]        <- your business logic                  |
|   [ Express ]         <- FRAMEWORK (optional)                 |
|   [ Node.js ]         <- RUNTIME (V8 + libuv + core modules)  |
|   [ JavaScript ]      <- LANGUAGE (ECMAScript spec)           |
|   [ Operating System ]<- kernel, syscalls, hardware           |
|                                                                |
+---------------------------------------------------------------+

If an interviewer asks "Is Node.js a programming language?", the answer is a confident "No. JavaScript is the language. Node.js is a runtime that lets JavaScript run outside the browser, with access to files, networks, and the operating system via V8 and libuv."


V8 + libuv — The Two Engines Inside Node.js

Node.js is not one thing. It's a carefully assembled sandwich of two major C++ projects, glued together by a thin JavaScript and C++ layer.

V8 — The JavaScript Engine

V8 is Google's open-source JavaScript engine, originally built for Chrome. Its job is to take JavaScript source code, parse it, compile it to highly optimized machine code, and execute it. V8 is also responsible for memory management (garbage collection) and implementing the core language features defined by ECMAScript.

V8 alone has no idea what a "file" is. It has no networking. It has no timers. If you embed V8 by itself in a C++ program, you can run JavaScript math, but you cannot read a file.

libuv — The Async I/O Library

libuv is a C library originally built for Node.js (now used by many projects). It provides the event loop, plus cross-platform asynchronous APIs for filesystems, networking, DNS, child processes, and timers. libuv also maintains a thread pool (default size: 4) for operations that can't be done asynchronously at the OS level — primarily filesystem operations and some CPU-bound tasks like crypto.pbkdf2.

This is the critical insight: Node.js is "single-threaded" only at the JavaScript level. Underneath, libuv runs a thread pool, the kernel runs async network I/O on its own threads, and V8 has its own background threads for garbage collection and compilation.

+---------------------------------------------------------------+
|           NODE.JS ARCHITECTURE                                |
+---------------------------------------------------------------+
|                                                                |
|   Your JavaScript Code                                         |
|        |                                                       |
|        v                                                       |
|   Node.js Core Modules (fs, http, net, crypto, ...)           |
|        |                                                       |
|        v                                                       |
|   Node.js Bindings (C++ glue code)                            |
|        |                      |                               |
|        v                      v                               |
|     [ V8 ]               [ libuv ]                            |
|     - Parse JS           - Event loop                         |
|     - JIT compile        - Thread pool (4 threads default)    |
|     - Garbage collect    - Async FS, net, DNS                 |
|                          - Timers                              |
|        |                      |                               |
|        +----------+-----------+                                |
|                   v                                            |
|           Operating System                                     |
|    (epoll on Linux, kqueue on macOS, IOCP on Windows)         |
|                                                                |
+---------------------------------------------------------------+

Code Example 1 — Proving It's Non-Blocking

Let's see the event loop in action. This is the "hello world" of understanding Node.js.

// blocking-vs-nonblocking.js
// Demonstrates that Node.js does NOT wait for I/O to finish
// before moving to the next line.

const fs = require('fs');

console.log('1. Program started');

// Non-blocking: Node.js hands this request to libuv and returns immediately.
// The callback will fire later, when the file has been read.
fs.readFile('./package.json', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('3. File has been read, length =', data.length);
});

// This line runs BEFORE the file is read, because readFile is async.
console.log('2. This line runs before the file is read');

// Expected output order:
// 1. Program started
// 2. This line runs before the file is read
// 3. File has been read, length = <some number>

Compare that to the blocking version, which would stop everything until the file is done:

// blocking-version.js
// Synchronous file read — the main thread STOPS here.
// Avoid this in a server. It freezes every other request.

const fs = require('fs');

console.log('1. Program started');

// Blocking: nothing else can run until the file is fully read.
const data = fs.readFileSync('./package.json', 'utf8');
console.log('2. File read, length =', data.length);

console.log('3. Program continues');

The async version uses libuv's thread pool for the filesystem operation. The sync version blocks the single JavaScript thread — and every incoming HTTP request has to wait for it to finish. In a server context, readFileSync is a scalability killer.


Code Example 2 — A Tiny HTTP Server (No Framework)

One of the most revealing Node.js demos: a fully functional HTTP server in 10 lines, with zero dependencies. This is pure runtime — no Express, no Fastify.

// server.js
// A complete HTTP server using only the built-in 'http' module.
// No framework. No npm install. This is Node.js itself.

const http = require('http');

// createServer returns a server object. The callback runs for every request.
const server = http.createServer((req, res) => {
  // Set a response header
  res.setHeader('Content-Type', 'application/json');
  // Send a JSON response and end the request
  res.end(JSON.stringify({ message: 'Hello from Node.js', url: req.url }));
});

// Start listening on port 3000. This registers a socket with libuv;
// the event loop will wake the main thread whenever a request arrives.
server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

Run it with node server.js. That single process can handle thousands of concurrent connections, because each request is just a callback — no threads, no forks.


Code Example 3 — Where Node.js Falls Apart (CPU-Heavy Work)

Here is the failure mode every interview loves to ask about.

// cpu-bound-disaster.js
// A deliberately bad example: heavy CPU work on the main thread.
// This will freeze the server for every other request until it finishes.

const http = require('http');

// A CPU-heavy function: calculate the nth Fibonacci number recursively.
// This is pure computation — no I/O, no async, just raw math.
function fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}

const server = http.createServer((req, res) => {
  if (req.url === '/fast') {
    // Fast route: instant response.
    res.end('fast');
    return;
  }
  if (req.url === '/slow') {
    // Slow route: blocks the event loop for several seconds.
    // While this runs, NO OTHER REQUEST can be handled.
    const result = fib(45);
    res.end(`fib(45) = ${result}`);
    return;
  }
  res.end('ok');
});

server.listen(3000);

Hit /slow in one tab and /fast in another at the same time. The /fast request will hang until /slow finishes, because the single JavaScript thread is busy doing math. The event loop can't rotate. Node.js's greatest strength — a single thread — is also its greatest liability when you give it CPU-bound work.

The fix is not "add more threads to JavaScript" — it's to move CPU-heavy work off the main thread using worker threads, child processes, or a dedicated service (Python, Go, Rust). We'll cover worker threads in Chapter 5.


When Node.js Is the Right Choice

Node.js shines when your workload is I/O-bound — meaning your server spends most of its time waiting on external systems, not crunching numbers.

  • REST and GraphQL APIs that mostly talk to a database, another API, or a cache. Each request is 5% logic, 95% waiting. Perfect for the event loop.
  • Realtime applications — chat, notifications, live dashboards, multiplayer game lobbies. WebSockets and long-lived connections are libuv's sweet spot. One process can hold tens of thousands of open sockets.
  • Streaming services — audio, video, file uploads, Server-Sent Events. Node's Stream API is built around backpressure-aware pipes.
  • Microservices and BFF layers that stitch together multiple upstream calls in parallel. Promise.all with async/await is a natural fit.
  • Build tools and CLIs — webpack, Vite, ESLint, Prettier, TypeScript compiler. Fast startup, great ecosystem, cross-platform.

When Node.js Is the Wrong Choice

Node.js is a poor fit when your workload is CPU-bound, meaning the server spends most of its time computing, not waiting.

  • Video/image transcoding, heavy image processing, ML inference on CPU. A single long computation freezes every request on the main thread.
  • Scientific computing, large matrix math, simulations, numerical solvers. Python (with NumPy/C extensions), Julia, or Rust will be faster and won't block other work.
  • PDF generation from scratch, large cryptographic operations, password hashing at scale (though libuv's thread pool helps here somewhat).
  • Long-running synchronous data processing. If the answer is "rewrite a 30-second for-loop," Node.js will happily block while it runs, and your 99th percentile latency will cry.

The honest rule of thumb: if your request spends more than ~10ms of pure CPU time, either offload it to a worker thread or pick a different runtime.


Node.js vs Browser JavaScript — Same Language, Different Universe

The JavaScript language is identical. The host environment is completely different.

+---------------------------------------------------------------+
|           BROWSER vs NODE.JS                                  |
+---------------------------------------------------------------+
|                                                                |
|                    Browser              Node.js                |
|                    -------              -------                |
|  Engine            V8 / SpiderMonkey    V8                     |
|  Global object     window               globalThis / global    |
|  DOM               YES (document)       NO                     |
|  Filesystem        NO                   YES (fs module)        |
|  Network           fetch (restricted)   http, net, tls, dgram  |
|  Module system     ES modules           CommonJS + ES modules  |
|  Sandboxing        Strong (same-origin) None (full OS access)  |
|  Timers            setTimeout           setTimeout + setImmediate|
|  Process / env     NO                   process.env, argv      |
|  Child processes   NO                   child_process          |
|                                                                |
+---------------------------------------------------------------+

The browser restricts JavaScript for security — you don't want a random web page reading your ~/.ssh/id_rsa. Node.js runs with the full privileges of the user, which is why require('fs').readFileSync('/etc/passwd') works on Linux if the process has permission.


Common Mistakes

1. Calling Node.js a "language" or a "framework" in an interview. Node.js is a runtime. JavaScript is the language. Express is a framework. Using these words interchangeably is the fastest way to look underprepared. Practice saying "the Node.js runtime executes JavaScript via V8 and uses libuv for async I/O" out loud until it's muscle memory.

2. Believing Node.js is "single-threaded" in the absolute sense. JavaScript runs on a single main thread, yes. But libuv has a thread pool, the kernel handles network I/O on its own threads, V8 runs garbage collection on background threads, and you can spawn worker_threads or child_process yourself. The accurate phrase is "single-threaded JavaScript execution on top of a multi-threaded C++ runtime."

3. Using synchronous APIs like fs.readFileSync in request handlers. Sync calls block the event loop, which blocks every other request in the same process. Reserve sync APIs for startup code (loading config, reading a cert file at boot). Never use them inside an HTTP handler.

4. Reaching for Node.js to solve CPU-heavy problems. If your app needs to resize 10,000 images per minute, Node.js is the wrong tool — or at least, the wrong default. Either offload to worker threads, use a native addon (sharp), or use a language better suited to CPU work. Don't fight the event loop.

5. Confusing Node.js global APIs with browser APIs. window, document, localStorage, and alert do not exist in Node.js. process, Buffer, __dirname, and require do not exist in the browser. Writing "universal" code requires knowing exactly which APIs come from the runtime and which come from the language.


Interview Questions

1. "What is Node.js? Is it a programming language?"

Node.js is a JavaScript runtime built on Google's V8 engine and the libuv async I/O library. It lets JavaScript execute outside the browser with access to the filesystem, network, child processes, and operating system APIs. It is not a programming language — JavaScript is the language, defined by the ECMAScript specification. It is also not a framework — frameworks like Express or NestJS are built on top of Node.js. In one sentence: Node.js is the environment that makes server-side JavaScript possible by combining V8 (for executing the language) with libuv (for non-blocking I/O).

2. "Explain the role of V8 and libuv inside Node.js."

V8 is the JavaScript engine originally built for Chrome. It parses JavaScript source code, JIT-compiles it to machine code, manages memory via garbage collection, and implements the core ECMAScript features. But V8 alone has no filesystem, no network, and no timers. libuv fills that gap. libuv is a C library that provides the event loop, cross-platform async I/O (epoll on Linux, kqueue on macOS, IOCP on Windows), DNS resolution, timers, and a thread pool of 4 threads by default for operations that can't be done async at the kernel level, like most filesystem calls. Node.js glues these two libraries together with C++ bindings and a JavaScript standard library (fs, http, net, crypto, etc.).

3. "Node.js is described as single-threaded but non-blocking. What does that actually mean?"

"Single-threaded" refers to the fact that your JavaScript code runs on one thread — there is exactly one call stack, and two pieces of JS never execute in parallel. "Non-blocking" means that when your code starts an I/O operation (reading a file, querying a database, making an HTTP request), the operation is handed off to libuv or the kernel, and the JavaScript thread immediately continues to the next task without waiting. When the I/O completes, libuv pushes a callback onto the event loop queue, and the JavaScript thread picks it up later. So the main thread is never idle — it's always either running JS or rotating the event loop. This is what lets one Node.js process handle thousands of concurrent connections with a small memory footprint. Important nuance: under the hood, libuv's thread pool and the kernel's network stack use multiple threads. It's only the JavaScript execution that is single-threaded.

4. "Why did Ryan Dahl build Node.js? What problem was he trying to solve?"

In 2009, Ryan Dahl was frustrated by how poorly existing servers handled concurrent connections. Apache and similar thread-per-request servers needed a dedicated OS thread for every client, and each thread consumed 1-2 MB of stack memory. Scaling to the now-famous C10K problem (10,000 concurrent connections) required gigabytes of RAM, and most threads spent their time blocked on I/O — doing no useful work. Dahl's insight was that an event-driven, non-blocking server could handle the same load with a single thread and a fraction of the memory, because most request time is spent waiting on databases, filesystems, and other services. He picked JavaScript because it already had first-class functions and closures (perfect for callbacks), had no existing I/O model to fight against, and had V8, which was a fast, open, embeddable engine. He presented Node.js at JSConf EU in 2009, and the rest is history.

5. "Give me a real-world case where Node.js is the wrong choice, and explain why."

Heavy video transcoding is a classic example. Imagine a service that takes user-uploaded videos and re-encodes them into multiple resolutions — that's minutes of pure CPU work per video. If you run it in the Node.js main thread, a single upload blocks the event loop and every other HTTP request hangs until the encode finishes. Even with async file I/O, the actual H.264 compression is CPU-bound, and the event loop can't help you. Better options: use a language better suited to CPU-bound work (Go, Rust, C++), call out to a specialized binary like ffmpeg via child_process, or offload jobs to a queue (Redis/Rabbit) processed by dedicated workers. The underlying principle: Node.js excels at I/O concurrency, not CPU parallelism. If your request spends most of its time computing instead of waiting, Node.js is probably the wrong default.


Quick Reference — Cheat Sheet

+---------------------------------------------------------------+
|           NODE.JS CHEAT SHEET                                 |
+---------------------------------------------------------------+
|                                                                |
|  WHAT IT IS:                                                   |
|  - Runtime (NOT a language, NOT a framework)                   |
|  - Executes JavaScript outside the browser                     |
|  - Built on V8 (engine) + libuv (async I/O)                    |
|                                                                |
|  EXECUTION MODEL:                                              |
|  - Single-threaded JavaScript execution                        |
|  - Non-blocking I/O via event loop                             |
|  - libuv thread pool (default 4) for FS and crypto             |
|  - Kernel async for network I/O (epoll/kqueue/IOCP)            |
|                                                                |
|  GOOD AT:                                                      |
|  - REST / GraphQL APIs                                         |
|  - Realtime (WebSockets, SSE)                                  |
|  - Streaming (video, audio, file uploads)                      |
|  - Microservices / BFF layers                                  |
|  - CLI and build tools                                         |
|                                                                |
|  BAD AT:                                                       |
|  - CPU-bound work (video encode, ML inference)                 |
|  - Heavy math / simulations                                    |
|  - Long synchronous computations                               |
|                                                                |
|  NEVER DO:                                                     |
|  - fs.readFileSync inside a request handler                    |
|  - while(true) loops on the main thread                        |
|  - Large JSON.parse of untrusted payloads without limits       |
|                                                                |
+---------------------------------------------------------------+

+---------------------------------------------------------------+
|           KEY TERMS                                            |
+---------------------------------------------------------------+
|                                                                |
|  V8         - Google's JS engine (parse, JIT, GC)              |
|  libuv      - C library: event loop + async I/O                |
|  Event loop - The scheduler that runs JS callbacks             |
|  Thread pool- 4 libuv threads for FS/crypto/DNS                |
|  Non-blocking - Operation returns immediately, callback later  |
|  I/O-bound  - Most time spent waiting on external systems      |
|  CPU-bound  - Most time spent computing                        |
|                                                                |
+---------------------------------------------------------------+
ConceptLanguageRuntimeFramework
ExampleJavaScriptNode.js, Browser, Deno, BunExpress, NestJS, Fastify
Defined byECMAScript spec (ECMA-262)V8 + libuv + core modulesOpinionated library code
Knows about files?NoYes (Node) / No (Browser)Depends on runtime
Knows about HTTP?NoYes (Node) / Limited (Browser)Yes (that's its job)
Replaceable?RarelySometimes (Node -> Bun)Often
FeatureBrowser JSNode.js
Global objectwindowglobalThis / global
DOMYesNo
fs moduleNoYes
http serverNo (only client)Yes
process.envNoYes
Module systemsES modulesCommonJS + ES modules
SandboxedYes (same-origin)No (full OS access)

Prev: Start of course Next: Lesson 1.2 -- The Event Loop Explained


This is Lesson 1.1 of the Node.js Interview Prep Course -- 10 chapters, 42 lessons.

On this page