βοΈ Node.js Runtime and Architecture
Node.js took JavaScript out of the browser and put it on the server, where it now powers APIs, real-time apps, and tooling for companies of every size. This lesson opens the hood: you'll see the V8 engine, the libuv library, and the event-driven, non-blocking model that lets a single thread juggle thousands of connections at once.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a JavaScript runtime is and how Node.js differs from browser JavaScript
- Describe the roles of the V8 engine and libuv inside Node.js
- Contrast event-driven, non-blocking I/O with the traditional thread-per-connection model
- Explain why Node.js is "single-threaded" yet uses a thread pool behind the scenes
- Decide when Node.js is a good fit for a project and when it is not
Estimated Time: 30β40 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Build and run a tiny HTTP server that routes on the URL path.
In This Lesson
What Is Node.js?
Node.js is a runtime that lets you execute JavaScript outside the browser β most commonly on a server. It bundles Google's V8 JavaScript engine (the same one inside Chrome) with a set of libraries for talking to the operating system: reading files, opening network sockets, handling timers, and more.
π‘ A useful analogy: Imagine you could only speak your native language while standing in your home country. Node.js is the passport that lets JavaScript travel anywhere β servers, build tools, command-line utilities, even robots β instead of being locked inside a web page.
Because the browser and Node.js both run on V8, the language is identical. What differs is the surrounding environment. A browser gives JavaScript the document, window, and DOM APIs. Node.js gives it file-system access, process information, and networking APIs instead.
Why Node.js Was Created
Node.js was released in 2009 by Ryan Dahl. He was frustrated that traditional servers such as Apache handled each connection by dedicating a whole operating-system thread to it. Threads consume memory and take time to switch between, so a server could be brought to its knees by a few thousand slow clients β even though most of those clients were just waiting for a file or a database.
π½οΈ The restaurant analogy: A thread-per-connection server is like a restaurant that hires one dedicated waiter per table. It works fine when the place is empty, but as it fills up you run out of waiters β and most of them are just standing around waiting for the kitchen. Node.js is the restaurant with one hyper-efficient waiter who takes an order, hands it to the kitchen, and immediately moves to the next table instead of standing still.
Dahl's insight was that servers spend most of their time waiting on I/O β disks, networks, databases β not computing. So instead of blocking a thread during every wait, Node.js registers a callback and moves on. That single idea is the foundation of everything below.
Inside the Architecture
Node.js is not a single program so much as a stack of cooperating layers. Your JavaScript sits at the top; underneath, C and C++ libraries do the heavy lifting of talking to the operating system.
fs Β· http Β· net Β· crypto] B --> C[V8 Engine
runs the JavaScript] B --> D[libuv
event loop & thread pool] C --> E[Operating System] D --> E
The V8 engine
V8 is Google's open-source JavaScript engine. Rather than interpreting your code line by line, it uses just-in-time (JIT) compilation to turn hot code paths into optimized machine code. That is a large part of why Node.js feels fast.
libuv
libuv is a C library that supplies the two features people most associate with Node.js: the event loop and a small thread pool. It provides a consistent, cross-platform way to do non-blocking network I/O and to offload work (like file access) that the OS cannot always do asynchronously.
π‘ Think of libuv as a personal assistant. It handles all the tedious waiting β watching for a file to finish loading, a socket to receive data, a timer to fire β and only interrupts you (your JavaScript) when there is a result ready to act on.
π Key Terms
Runtime: the environment that executes your code and gives it built-in capabilities.
Event loop: the coordinator that watches for completed operations and runs their callbacks.
I/O: input/output β reading/writing files, network requests, database calls; anything that leaves the CPU.
Non-Blocking I/O Explained
The phrase you'll hear constantly is "event-driven, non-blocking I/O." Here is what it means in practice, using a coffee shop:
- Blocking (synchronous) model: the barista takes one order, makes the drink to completion, hands it over, and only then serves the next person. Everyone waits in a single line.
- Non-blocking (Node.js) model: the barista takes an order, starts the machine, and immediately takes the next order. As each drink finishes, they call out a name. The line keeps moving because nobody stands idle waiting.
asynchronously] C -->|File / crypto| E[Thread Pool] C -->|Pure JS| F[Run immediately] D --> G[Callback queued] E --> G G --> B B --> H[Response to client]
When Node.js needs to read a file, it does not sit and wait. It hands the request to libuv, registers a callback, and returns to the event loop to serve other requests. When the read completes, the callback is queued, and the event loop runs it. The CPU is never blocked staring at a spinning disk.
β Why this scales
Because waiting is "free" (it costs no thread), a single Node.js process can keep tens of thousands of connections open at once. This is exactly the workload β many connections, each mostly idle β that overwhelms thread-per-connection servers.
Single-Threaded, Not Limited
A common misconception is that Node.js is entirely single-threaded. It's more precise to say your JavaScript runs on a single thread β the one that runs the event loop. But libuv keeps a small thread pool (four threads by default) for operations the OS can't do asynchronously, such as file system access, DNS lookups, and some cryptographic work.
π¨ The hotel receptionist: The event loop is a receptionist who answers every guest personally for quick questions. For anything slow β cleaning a room, fixing plumbing β they dispatch a staff member (a thread-pool worker) and keep helping other guests. When the task is done, the worker reports back and the receptionist notifies the guest.
You can resize this pool with the UV_THREADPOOL_SIZE environment variable (up to 1024). For truly CPU-heavy JavaScript work β image processing, large computations β Node.js also offers worker threads, which run JavaScript in parallel on separate threads. We'll return to those in a later lesson.
β οΈ The one thing that can hurt you
Because your JavaScript shares one thread, a long synchronous loop blocks everything β no other request can be served until it finishes. The golden rule of Node.js is: don't block the event loop. Keep synchronous work short and push heavy computation to worker threads.
When to Use Node.js
Node.js shines for I/O-bound workloads and struggles with sustained CPU-bound ones. Match the tool to the job:
| Great fit β | Poor fit β οΈ |
|---|---|
| REST & GraphQL APIs, microservices | Video encoding / heavy image processing |
| Real-time apps (chat, live dashboards, games) | Long-running scientific computation |
| Streaming services (video/audio proxies) | Sustained number-crunching without workers |
| Backends for SPAs (React, Vue, Angular) | Tasks that truly need many parallel CPU cores |
| Command-line tools and build tooling |
A well-known real-world example: Netflix and PayPal both adopted Node.js for parts of their stack and reported faster startup and higher throughput for their connection-heavy, I/O-bound services. The lesson is not "Node is always best" β it's "Node is excellent for the very common case of shuffling data between clients and databases."
Node.js vs. traditional servers
| Feature | Node.js | Traditional (Apache + PHP) |
|---|---|---|
| Concurrency model | Single thread + event loop | One thread (or process) per connection |
| I/O model | Non-blocking, asynchronous | Typically blocking, synchronous |
| Many idle connections | Very efficient | Memory-heavy |
| Language on both ends | JavaScript front and back | Different languages |
Worked Example: A Real Server
The classic "hello world" server is only three useful lines. Here it is with modern syntax and a note on what each part does:
// server.js β a minimal HTTP server using the built-in http module
const http = require('node:http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Hello from Node.js!</h1>');
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
That responds to every request the same way. A real server usually routes on the URL. Here is a slightly larger version that answers three different paths and returns JSON where appropriate:
const http = require('node:http');
const server = http.createServer((req, res) => {
// req.url is the path the browser asked for, e.g. "/about"
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Home</h1><p>Welcome!</p>');
} else if (req.url === '/time') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ now: new Date().toISOString() }));
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
}
});
server.listen(3000, () => {
console.log('Listening on http://localhost:3000/');
});
Visiting /time returns:
{ "now": "2026-07-31T12:00:00.000Z" }
Notice the node: prefix on require('node:http'). This is the modern, recommended way to import built-in modules β it makes it unmistakable that you mean Node's own http and not some package named "http" from npm.
Hands-on Exercise
ποΈ Build a Multi-Route Server
Objective: Prove to yourself that a bare Node.js server can route requests.
Instructions:
- Install Node.js from nodejs.org (choose the LTS version).
- Create a file named
server.jsand paste the multi-route example above. - Run it with
node server.jsand visithttp://localhost:3000/and/timein your browser. - Add a new route: make
/aboutreturn an HTML page with your name and a one-line bio. - Add a
/healthroute that returns{ "status": "ok" }as JSON.
π‘ Hint
Add another else if (req.url === '/about') branch before the final else. Remember to set Content-Type: application/json and use JSON.stringify() for the health check, just like the /time route.
β Example solution
const http = require('node:http');
const server = http.createServer((req, res) => {
if (req.url === '/about') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>About Ray</h1><p>Full stack learner building small servers.</p>');
} else if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
}
});
server.listen(3000, () => console.log('Ready on :3000'));
π― Quick Quiz
Question 1: Which JavaScript engine does Node.js use to execute code?
Question 2: Why can a single Node.js process handle so many concurrent connections?
Question 3: Which workload is the poorest fit for a plain Node.js server?
Summary & Quiz
π Key Takeaways
- Node.js is a runtime that runs JavaScript outside the browser, built on the V8 engine plus libuv.
- Its event-driven, non-blocking I/O model lets one thread serve huge numbers of mostly-idle connections.
- Your JavaScript runs on a single thread, but libuv's thread pool handles file, DNS, and crypto work behind the scenes.
- Node.js excels at I/O-bound work (APIs, real-time, streaming) and is a poor fit for sustained CPU-bound work unless you use worker threads.
- The golden rule: don't block the event loop.
π Further Reading
π What's Next?
Next we'll explore the core modules Node.js ships with β fs, path, events, and more β and the enormous npm ecosystem of third-party packages that lets you build almost anything without starting from scratch.
π Nice work!
You now understand what makes Node.js tick. Let's put it to work with real modules.