How Node.js Works

The JavaScript runtime that lets JavaScript run outside a browser, and the single-threaded, event-driven model behind it.

What is it?

JavaScript was originally built to run inside a browser, reacting to clicks and typing. Node.js is a runtime that lets the same language run anywhere else — on a server, handling incoming HTTP requests instead of clicks. It does this by pairing Chrome's V8 engine (which turns JavaScript into fast machine instructions) with extra capabilities a browser doesn't need, like reading files from disk or opening network connections.

The part that surprises people most is that Node runs your JavaScript on a single thread — it can only execute one line of your code at a time. It stays fast under load anyway because most of what a backend does (reading a file, querying a database, waiting for a network response) is waiting, not computing. Instead of blocking that one thread while it waits, Node hands the waiting off and moves on to other work, coming back to your code only once the result is ready. That loop — run some code, hand off anything that waits, run whatever's ready next — is the event loop.

Explain like I'm 10

Node.js is like a single waiter working an entire restaurant. Instead of standing at one table until the kitchen finishes that order, the waiter takes the order, moves straight to the next table, and comes back to serve each dish the moment it's ready. One waiter can serve far more tables this way than by camping out at each one — as long as no single task (like arguing with a customer for twenty minutes) hogs the waiter's attention.

Examples

Blocking vs. non-blocking file reads

const fs = require("fs");

// Blocking: nothing else runs until this finishes
const data = fs.readFileSync("large-file.txt");
console.log("done reading");

// Non-blocking: Node moves on immediately, and runs
// this callback later, once the file is ready
fs.readFile("large-file.txt", (err, data) => {
  console.log("done reading");
});
console.log("this logs first, before the read finishes");

The sync version freezes the single thread until the disk read completes; the async version hands the read off and keeps running other code, coming back only when the result is ready.

Why this matters for a server

app.get("/report", (req, res) => {
  // While this database query is pending, Node is
  // free to handle other incoming requests on the
  // same single thread.
  db.query("SELECT * FROM big_table", (err, rows) => {
    res.json(rows);
  });
});

A slow database query for one user doesn't block the server from responding to other users in the meantime — the thread isn't sitting idle waiting for that query.

How it works

Node runs your JavaScript on one main thread. When your code calls something that involves waiting — reading a file, querying a database, making an HTTP request — Node doesn't do that waiting on the main thread itself. It hands the work off (to the operating system, or to a background thread pool inside Node called libuv) and immediately continues running whatever JavaScript comes next.

Once that background work finishes, its callback (or promise) is placed in a queue. The event loop is the process that continuously checks: "is the main thread free, and is there a finished callback waiting?" — and when both are true, it runs that callback. This is why a single Node process can have thousands of database queries or file reads "in flight" at once, even though it only ever executes one line of your JavaScript at any given instant.

Your code            Background (libuv / OS)
 ─────────            ───────────────────────
 fs.readFile() ──────▶  reading disk...
    │
    ▼
 (keeps running
  other requests)
                        disk read finishes
                              │
                              ▼
 callback queued ◀────────────
    │
    ▼
 event loop runs it
 once the thread is free

Why does it exist?

Traditional server designs often gave each incoming connection its own thread, which works but gets expensive — thousands of connections mean thousands of threads, each with real memory and scheduling overhead. Node exists to handle huge numbers of I/O-bound connections (network and disk work, not heavy computation) efficiently on a single thread, by never letting the thread sit idle waiting for something slow.

When to use it

Node is a strong fit for backends that spend most of their time waiting on I/O — REST APIs, real-time apps (chat, live dashboards), and services that mostly shuttle data between a client and a database or another service.

When not to use it

Node struggles with CPU-heavy work — image or video processing, large in-memory computation, cryptographic hashing of huge payloads — because that kind of work occupies the single thread and blocks everything else until it finishes. For that, you'd offload the work to worker threads, a separate service, or a language built around multi-threaded computation.

Common mistakes

  • Running a long, synchronous, CPU-heavy loop inside a request handler, which freezes the entire server for every other user until it finishes.

  • Assuming Node is multi-threaded because it can 'handle' many requests at once — it's the waiting, not your actual JavaScript, that happens concurrently.

  • Using a synchronous file system method (like readFileSync) inside a request handler in a production server, blocking every other request while it runs.

Practice exercises

  1. Easy:

    Explain, in one sentence, why fs.readFile lets other code run sooner than fs.readFileSync.

  2. Medium:

    Write a small snippet with three console.log calls — one before, one inside a setTimeout callback, and one after — and predict the order they print in.

  3. Hard:

    Explain why a single expensive, synchronous loop (like sorting a huge array) inside one request handler would slow down responses to every other concurrent user, even though Node is handling many requests.

Interview questions

Is Node.js single-threaded or multi-threaded?

Your JavaScript runs on a single main thread, but Node offloads I/O work (file access, network calls) to the operating system or a background thread pool, so it can appear to handle many things concurrently.

What is the event loop?

The mechanism that continuously checks whether the main thread is free and whether any background work has finished, and runs the corresponding callback when both are true.

Why is a CPU-heavy operation bad for Node.js performance?

It occupies the single main thread directly, so nothing else — including responses to other users — can run until it finishes, unlike I/O work which is handed off elsewhere while it waits.

What is V8, and what job does it do inside Node.js?

V8 is Chrome's JavaScript engine; inside Node it's the component that actually parses and executes your JavaScript, turning it into fast machine instructions — Node pairs V8 with extra APIs (filesystem, networking) that a browser wouldn't need.

What does libuv do, and why does Node need it if JavaScript runs on a single thread?

libuv is the background layer that provides Node's event loop and manages a thread pool for operations that can't be handled asynchronously by the operating system alone (like some filesystem calls); it lets Node hand off waiting work without blocking the single JavaScript thread, then notify that thread when the work completes.

Between `fs.readFileSync(...)` followed by a `console.log`, and `fs.readFile(..., callback)` followed by the same `console.log`, which logs first relative to the file finishing?

With readFileSync, the log runs only after the entire file has been read, since the call blocks the thread; with readFile, the log after it runs immediately, before the file finishes reading, since the async version returns right away and the callback runs later once the read completes.

Why doesn't a slow database query for one user necessarily block the server from responding to other users at the same time?

The query is handed off to the database (and effectively to the background/OS layer waiting on it) rather than run on Node's main thread, so the main thread stays free to process other requests while it waits for that query's result.

A request handler runs a synchronous loop sorting a 10-million-item array. What happens to every other concurrent request while it runs?

All other requests are stalled, because the single main thread is fully occupied executing that loop and cannot process the event loop's queue or any other JavaScript until the loop finishes.

Why is it misleading to say "Node.js is multi-threaded because it can handle many requests concurrently"?

The concurrency comes from I/O waiting being handed off elsewhere, not from your JavaScript actually executing on multiple threads at once — your own code still runs one line at a time on a single thread; only the waiting happens in parallel, not the computing.

What actually happens, mechanically, when your code calls an asynchronous function like `fs.readFile`?

Node starts the operation and hands it off to the operating system or a background thread pool (via libuv), then immediately returns control to your code so the next line can run; once the background work finishes, its callback is placed in a queue for the event loop to run once the main thread is free.

Why does Node.js tend to perform well for REST APIs and real-time apps, but poorly for heavy image or video processing done directly in a request handler?

REST APIs and real-time apps spend most of their time waiting on network or database I/O, which Node handles efficiently by not blocking the thread; image and video processing is CPU-bound computation that occupies the thread directly, blocking everything else for as long as it runs.

What would you reach for instead of doing CPU-heavy work directly in a Node.js request handler?

Worker threads, a separate service or process dedicated to that computation, or a language/runtime built around multi-threaded computation — offloading the work so the main thread stays free to keep handling other requests.

One endpoint calls `JSON.parse` on a huge synchronous payload directly in its handler, and the whole server feels sluggish. Why could one endpoint slow down unrelated requests?

JSON.parse on a large payload runs synchronously on the main thread; while it's parsing, the event loop can't process anything else, so every other request — even ones hitting completely different endpoints — has to wait for that parsing to finish.

Why does the event loop need to check both "is the main thread free" and "is there a finished callback waiting," rather than running callbacks the instant their background work completes?

The main thread can only execute one thing at a time, so a finished callback can't interrupt code that's currently running — it has to wait in a queue until the thread becomes free, at which point the event loop picks it up.

Compare a traditional thread-per-connection server design to Node's single-thread-plus-event-loop design. What cost does Node avoid at scale?

Thread-per-connection designs create real memory and OS scheduling overhead for every simultaneous connection, which grows expensive with thousands of connections; Node instead handles many connections on one thread by never leaving it idle waiting on I/O, avoiding the cost of spinning up and scheduling huge numbers of threads.

Why is `fs.readFileSync` considered dangerous inside a production server's request handler, but safe in a one-off setup script?

In a request handler, blocking the single thread stalls every other concurrent request being served by that same process; in a one-off script there's nothing else concurrently relying on that same thread, so blocking it briefly has no such cost.

If Node hands off waiting for I/O elsewhere, what part of the code still has to run entirely on the single main thread with no way around it?

Your actual JavaScript logic — any computation, loop, or synchronous function call you write — always executes on the single main thread; only the waiting for external operations (disk, network, database) is what gets handed off.

Three `console.log` calls: one before `setTimeout(fn, 0)`, one inside `fn`, and one right after the `setTimeout` call. What order do they print in?

The first log, then the third log, then the second log inside the callback — setTimeout always defers its callback until at least the current synchronous code finishes running, even with a 0ms delay, so the code immediately after the setTimeout call runs before the callback does.

Why does Node.js pairing V8 with extra APIs matter — what could JavaScript not do inside a browser that Node needed to add?

Browser JavaScript is sandboxed and has no direct access to the filesystem or the ability to open arbitrary network server sockets, for security reasons; Node adds APIs like fs and net/http on top of V8 specifically so the same language can do server-appropriate things a browser would never allow.

What's the relationship between "waiting" and "computing" in explaining why Node stays responsive under many concurrent connections despite being single-threaded?

Most of what a backend does per request is waiting (on disk, network, or a database), not computing — Node stays responsive because it never lets the thread sit idle during that waiting, freeing it to do other requests' computing in the meantime, so many requests appear to progress at once even though actual computation still happens one step at a time.