NodeJSHow Node.js Works

How Node.js Works

To use Node.js well — and to debug it when things go sideways — you need an accurate mental model of what happens when you run node app.js. This page builds that model from the ground up: the components involved, what happens at startup, how a single asynchronous operation flows through the system, and why "single-threaded" does not mean "one thing at a time."

The components

Node.js is essentially four pieces glued together. The first two do the heavy lifting:

Component

Language

Responsibility

V8

C++

Parses, compiles, and runs your JavaScript; manages the heap and garbage collection

libuv

C

Provides the event loop, the thread pool, and cross-platform async I/O

C++ bindings

C++

Glue connecting JavaScript calls to V8 and libuv

Core JS libraries

JavaScript

The APIs you import — fs, http, crypto, events, stream, ...

Each gets its own deep dive: The V8 JavaScript Engine and libuv & the C++ Core.

What happens when you run \`node app.js\`

Startup is a sequence of concrete steps:

  • 1. Node initializes V8 (creating a heap and a "context") and initializes libuv (creating the event loop and a thread pool, by default 4 threads).

  • 2. It wraps your file in the module wrapper function so require, module, exports, __dirname, and __filename are available.

  • 3. V8 compiles your script to bytecode and starts executing it top to bottom on the single main thread. All synchronous code runs to completion here.

  • 4. As your code calls asynchronous APIs (setTimeout, fs.readFile, server.listen), Node registers callbacks and hands the underlying work to libuv or the OS.

  • 5. When the top-level script finishes, Node enters the event loop. It keeps running as long as there are pending callbacks, timers, or open handles (like a listening server).

  • 6. When nothing is left to do, the loop exits and the process ends with exit code 0.

The life of an asynchronous operation

Follow a single fs.readFile call all the way through. This is the flow that repeats millions of times in a running server:

fs.readFile('data.txt', cb)

Text
Your JS  ──calls──►  fs.readFile(path, cb)
   │
   ▼
C++ binding  ──hands work to──►  libuv
                                   │
                                   ▼
                          thread pool reads the file
   (meanwhile your main thread keeps running other code!)
                                   │
                            read completes
                                   ▼
                       libuv queues your callback
                                   │
                                   ▼
   event loop (poll phase)  ──runs──►  cb(err, data)   ← back in V8

The crucial moment is in the middle: while the file is being read, your main thread is not waiting. It is free to accept new requests, run timers, or process other callbacks. Node only runs your JavaScript again when there is a finished result to hand back.

Synchronous, then microtasks, then macrotasks

The order in which queued callbacks run is precise and worth memorizing. Consider:

order.js

JS
console.log('1: start')

setTimeout(() => console.log('5: setTimeout'), 0)     // macrotask (timer)
setImmediate(() => console.log('6: setImmediate'))     // macrotask (check)

Promise.resolve().then(() => console.log('4: promise')) // microtask
process.nextTick(() => console.log('3: nextTick'))       // runs before microtasks

console.log('2: end')
1: start
2: end
3: nextTick
4: promise
5: setTimeout
6: setImmediate

All synchronous code runs first (1, 2). Then, after each operation, Node drains the process.nextTick queue (3), then the microtask queue of resolved promises (4), and only then does the event loop move through its phases — timers (5) and the check phase (6). The full set of phases is detailed in The Event Loop; the nextTick/microtask priority is covered in setImmediate & process.nextTick.

"Single-threaded" — but not alone

Your JavaScript runs on exactly one thread: the main thread. But Node is not single-threaded as a process. libuv maintains a thread pool (4 threads by default, configurable with UV_THREADPOOL_SIZE) for work that the OS cannot perform asynchronously on its own — file system operations, DNS lookups via getaddrinfo, and CPU-heavy crypto/zlib calls.

Operation

How libuv handles it

Uses a pool thread?

TCP/HTTP networking

OS kernel async (epoll/kqueue/IOCP)

No

File system (fs)

Thread pool

Yes

DNS lookup

Thread pool

Yes

crypto.pbkdf2, zlib

Thread pool

Yes

setTimeout / timers

Loop timers phase

No

The key insight
Node achieves high concurrency not by spawning many threads, but by **never blocking** the one thread that runs your code. Slow work is delegated to the kernel or the thread pool; your code only runs when there is a result to handle. This is why thousands of idle connections cost almost nothing.
Why blocking is the cardinal sin

Because one thread serves all requests, a single slow synchronous operation freezes every client simultaneously. Compare a non-blocking read with a CPU loop that hijacks the thread:

A blocking handler stalls the entire server

JS
import http from 'node:http'

http.createServer((req, res) => {
  if (req.url === '/slow') {
    // BAD: a 3-second busy loop — no other request is served meanwhile.
    const end = Date.now() + 3000
    while (Date.now() < end) {}
    res.end('done blocking')
  } else {
    res.end('fast')   // during /slow, even this cannot respond
  }
}).listen(3000)
Diagnosing a frozen loop
If your server intermittently "hangs," suspect a blocked event loop: a synchronous `*Sync` call, a giant `JSON.parse`, a heavy regex, or a long computation in a request handler. The fix is to make it asynchronous, chunk it, or offload it to [Worker Threads](/nodejs/worker-threads) or a [child process](/nodejs/child-processes).
Putting it together
  • Synchronous code runs immediately, top to bottom, on the main thread.

  • Async APIs delegate work and register a callback — they never block.

  • The event loop runs queued callbacks when the main thread is free.

  • nextTick and microtasks run between phases, before the loop advances.

  • Networking uses the kernel; files/DNS/crypto use the thread pool.

  • Keep per-callback work small — the loop must keep turning.

Next
Dive into the pieces: [The V8 JavaScript Engine](/nodejs/v8-engine), then [libuv & the C++ Core](/nodejs/libuv), then the [Single-Threaded, Non-Blocking I/O](/nodejs/single-threaded-model) model.