Skip to main content

Command Palette

Search for a command to run...

Deep dive into Node.JS Architecture

Updated
19 min read
Deep dive into Node.JS Architecture
D
"Hello, I am Dipali. Currently a student of ChaiCode web development cohort✨"

Introduction

In today’s fast-paced, data-driven world, the demand for responsive, real-time web applications has increases. Whether you’re scrolling through instagram reels or social media feed, booking a uber/ola cab, or chatting in a support window, there’s a good chance Node.js is the engine under the hood.

To understand why Node.js is the "engine" behind your favorite apps, it helps to look at it not as a programming language, but as a highly efficient traffic controller.

let's understand with simple example,

Imagine you are building a group chat like WhatsApp.

  • Traditional Server: Every time someone sends you a message, the server creates a "connection" and stays "busy" holding that connection open. With 10,000 users, the server runs out of memory and crashes.

  • Node.js Server: It uses WebSockets ( it is a way to create a persistent, real-time connection between a client and a server). It keeps one tiny, lightweight "door" open for everyone. When a message comes in, the "Event Loop" simply broadcasts it to everyone else instantly without needing to start a new process.

Node.js isn't the best for "heavy lifting" (like editing a 4K video), but it is the world champion of "fast talking" (handling thousands of small, real-time data exchanges).


The Historical Catalyst: The C10k Problem

The C10k Problem was a major turning point in the evolution of modern web servers and backend technologies.

The term C10k was coined in 1999 by software engineer Dan Kegel, when he asked a simple but powerful question:

“How can we handle 10,000 simultaneous client connections on a single server?”

Back in the late 1990s:

  • Most servers used a thread-per-connection model, example traditional servers like Apache spawned a new thread for every single connection.

  • Each client request = one thread

  • Threads were expensive (memory + CPU overhead)

The issue:

  • Each thread consumes a significant amount of RAM (roughly 1MB to 2MB)

  • If you have 10,000 users →you need 10,000 threads (20GB of RAM) just to keep the connections open, even if they aren't doing anything.

  • This led to:

    • High memory usage

    • Context switching overhead

    • Poor scalability

    • Frequent crashes under load

Node.js solved this by using a single thread to manage thousands of connections via non-blocking I/O.

Think of Non-Blocking I/O is like a Secret Sauce of your kitchen:

In a "blocking" system, the waiter stands at the kitchen door waiting for your biryani to cook, refusing to help anyone else. In Node's Non-blocking system, is like the waiter takes your order, puts it in the kitchen, and serves five other tables while your biryani sizzling with ghee and fragrant saffron rice.


A Brief History of Node.js

Node.js was created in 2009 by Ryan Dahl at the JSConf EU 2009.

His goal was to solve a major limitation in web development, browsers handled JavaScript well, but servers relied on heavier, blocking technologies like PHP, Java, and Ruby.

So, Node.js was built on the powerful Google Chrome V8 engine, allowing JavaScript to run outside the browser for the first time in a scalable way.

Node.js introduced an event-driven, non-blocking architecture, making it highly efficient for handling thousands of simultaneous connections.

This innovation directly addressed issues like the C10k problem.

Rise of npm (2010)

In 2010, Isaac Z. Schlueter created npm (Node Package Manager).
npm quickly became the largest ecosystem of open-source libraries, making Node.js extremely attractive to developers.

Today, Node.js is widely used for building fast, scalable web applications and APIs.

NodeJS started to gain popularity, big companies like,

Company

The Problem

The Node.js Solution

Netflix

Slow startup times and heavy Java backend.

Switched to Node.js and reduced startup time by 70%.

PayPal

Developers had to write Java for the back and JS for the front.

Unified the stack. Built apps 2x faster with 33% fewer lines of code.

Uber

Needed to process massive amounts of data in real-time (tracking drivers).

Used Node.js to handle millions of concurrent connections with zero lag.

Walmart

Site used to crash during Black Friday traffic spikes.

Moved to Node.js; on Black Friday, their servers handled 1.5 billion requests without breaking.


The Great Schism: Node.js vs. io.js (2014–2015)

The "Great Schism" of Node.js is a classic tale of open-source drama, governance, and eventually, a very successful reunion. It’s the moment the community decided that the project’s future was too important to be controlled by a single corporate entity.

The Gathering Storm (2014)

Around 2014, when frustration grew in the Node.js community because development had slowed under Joyent, the company maintaining Node.js at the time.

  • Node.js hadn't seen a major release in ages, Slow updates and lack of transparency (stuck on v0.10 and v0.12).

  • Only a tiny group of Joyent employees had "commit bit" (the power to merge code).

  • The V8 engine was evolving rapidly, but Node.js was lagging behind, failing to support new ES6 features that developers were craving.

io.js: an alternative to Node

In December 2014, Fedor Indutny and other core contributors "forked" the project to create io.js. This wasn't just a rename, it was a revolution in how the project was run.

io.js basically focused on:

  • Faster releases

  • Latest JavaScript features (via updated V8 engine)

  • Open governance (community-driven decisions)

The Great successful reunion (2015)

The fork was a massive wake-up call for Joyent. They realized that if the community left, the Node.js "brand" would become a ghost town.

By mid-2015, negotiations led to the formation of the Node.js Foundation a neutral, non-profit home for the project. This allowed the two projects to merge back together.

This led to:

  • Merging io.js back into Node.js

  • Adopting open governance

  • Faster and more predictable release cycles, io.js became Node.js v4.0.0 (skipping versions 1, 2, 3).

Fun Fact: Because io.js moved so fast, it actually released versions 1.0, 2.0, and 3.0 during the few months it existed separately, while original Node.js was still stuck at v0.12! "It’s a rare example of a "fork" actually saving a project rather than killing it."

Comparison Table of Node.js vs. io.js

Feature Node.js (then) io.js
Governance Controlled by Joyent Open & community-driven
Release Cycle Slow Fast
Features Conservative Cutting-edge

The Core Architecture: Under the Hood

Node.js relies on various dependencies under the hood for providing various features.

1 . The V8 Engine

V8 is the JavaScript execution engine which was initially built for Google Chrome. The V8 engine is a core component of Node.js that executes JavaScript code. It compiles JavaScript directly into machine code to improve performance and execution speed.

  • Developed by Google and written in C++, designed to efficiently execute JavaScript code.

  • Used in both Google Chrome and Node.js, enabling JavaScript to run in browsers and on the server side.

  • Compiles JavaScript directly into machine code, which improves execution speed compared to traditional interpretation.

How V8 Engine Works:

Since Node.js runs on Google’s V8 engine, it's manages memory using two main regions:

  • The Stack: Used for static data. This includes primitive values (numbers, booleans) and pointers to objects. It follows the LIFO (Last-In-First-Out) principle. Access is lightning-fast because the engine knows the exact size of the data at compile time.

  • The Heap: Used for dynamic data. This is where objects, arrays, and closures live. Since their size can change, V8 allocates a large, messy block of memory. The Stack holds a "pointer" (an address) that tells the engine where to find the actual object in the Heap.

So, The V8 engine is responsible for the heavy lifting of executing your code. It specifically manages:

Your Heap and the Call Stack and also handle the management of your memory.

  • Call Stack: It executes synchronous code. This is where V8 keeps track of "Where we are" in the code. It follows LIFO (Last in, First Out). When you call a function, it's pushed onto the stack. when it returns, it's popped off.

Features of V8 Engine

The V8 engine comes with several important features that contribute to the performance and efficiency of Node.js applications:

  • Just-In-Time (JIT) Compilation: It converts JavaScript directly into machine code at runtime. No need for an interpreter → faster execution.

  • Garbage Collection (Orinoco): When you use a website, the browser creates objects in its memory (like images, variables, or pieces of text). Once you click away or close a menu, those objects are no longer needed. If the browser didn't clean them up, your computer would eventually run out of memory and crash. Orinoco is the system that finds that "trash" and throws it away to keep your computer running smoothly. It known as Garbage Collection. In my mother tongue "Jo kam ka nhi use phek do".

  • Runtime Optimization: is the process of turning your generic JavaScript into highly specialized, lightning-fast machine code while the program is actually running.

  • ES6+ Support: It supports modern JavaScript features such as classes, promises, template literals, and async functions.

V8 only understands the JavaScript language itself (variables, functions, objects). It has no built-in way to talk to the internet network, read a file from your hard drive, or even wait for 5 seconds your timers.


2 . The Libuv Thread Pool

When i first hear the name libuv it's remind that popular Chinese doll but her name is Labubu yeah i know it's not same but steal i think of her and it's really scary. if you don't know about that doll. so, don't worry i attached the photo of her. scary right!

Let's back to over topic...

What is Libuv? (The Engine Powering Node.js)

Technically speaking, libuv is a multi-platform C library that provides support for asynchronous I/O based on event loops. Originally designed for Node.js, it has now become a vital tool in many other software projects. It’s designed to be both highly efficient and cross-platform, running on Linux, Windows, Mac OS X, and more, offering a consistent API across all these platforms.

Think of libuv as the unsung hero working behind the scenes of Node.js. While you write JavaScript, libuv is the "engine room" that actually talks to your computer’s operating system to get things done.


Real-World Applications of Libuv

While Libuv is widely recognized for its role in Node.js, it powers many other projects across different domains. Some notable examples include:

  • Julia – A high-performance language for technical computing.

  • CMake – A popular build system generator.

  • BIND 9 – A widely used open-source DNS server.

  • Neovim – A modern reimplementation of Vim with extensive plugin support.

Libuv’s capabilities make it indispensable for performance-sensitive applications, particularly those that require efficient asynchronous operations. Many real-time systems, streaming platforms, and cloud-based applications rely on Libuv’s event-driven architecture to handle large-scale data processing with minimal latency.


The Event Loop

This is the heart of Libuv and those event loop is a game-changer for handling multiple operations concurrently. Before event loops were common, servers typically handled each request by spawning a new thread, which led to significant scalability issues. With Libuv, developers can register I/O operations with the event provider, and once an operation is complete, it triggers a callback function. This non-blocking approach enables applications to handle thousands of simultaneous requests efficiently.

1 . Timers Phase: The loop starts here. It checks if any setTimeout or setInterval timers have expired.

Note: The delay you specify is the minimum time, not the exact time. If the loop is busy in another phase, your 10ms timer might take 15ms to execute.

2 . Pending Callbacks Phase: This phase executes I/O callbacks that were deferred from the previous loop iteration. For example, certain types of TCP errors (like ECONNREFUSED) are reported here.

3 . Idle / Prepare Phase: This is used internally by Node.js for housekeeping. You will rarely, if ever, interact with this phase as a developer.

4 . Poll Phase (The Heart of Node): This is where the loop spends most of its time. It does two things:

  • Process the I/O Queue: It executes callbacks for almost everything (File I/O, Network I/O, etc.).

  • Calculate Block Time: If the queue is empty, the loop will actually pause and wait here for new I/O events to come in from the OS, rather than spinning uselessly.

5 . Check Phase: This phase is specifically for setImmediate() callbacks. If you want a piece of code to run immediately after the Poll phase finishes its current batch of I/O, you put it here.

6 . Close Callbacks Phase: If a socket or handle is closed abruptly (e.g., socket.destroy()), the 'close' event is emitted here.


The Microtask Queues (The High-Priority Lane)

There are two special queues that are not part of Libuv’s 6 phases but are handled by the V8 engine:

  • process.nextTick()

  • Promises (.then(), async/await)

These are called Microtasks. They have higher priority than the Event Loop phases. Node.js will stop the loop and empty the Microtask queue immediately after any phase (and even during the middle of some phases) before moving to the next one.


What is a Thread pool?

While the Event Loop handles "non-blocking" tasks (like network requests), some tasks are "blocking" or too heavy for the loop (like file system operations or heavy encryption). Libuv maintains a pool of threads. Libuv assigns tasks to a pool of worker threads.

The thread pool in libuv is a pool of worker threads (default size: 4) that offloads expensive or blocking operations.

These include:

  • File system operations (fs.readFile, fs.writeFile)

  • DNS lookups (dns.lookup)

  • Cryptographic functions (crypto.pbkdf2, crypto.scrypt)

  • Compression/decompression (zlib.gzip)

When such an operation is triggered, it’s passed to the thread pool, so the main event loop isn’t blocked.

However, all callbacks that occur on task completion are executed on the main thread.

After Node 10.5 worker threads can also be used to execute JavaScript in parallel. Libuv uses 4 threads by default, but can be changed using the UV_THREADPOOL_SIZE

process.env.UV_THREADPOOL_SIZE = 5

The Operating System Layer

This is the lowest level where the actual work happens. Node.js tries to be as efficient as possible by using Native Async I/O whenever it can.

  • OS Native Async (Epoll/Kqueue/IOCP): For network requests, Node doesn't even use its own Thread Pool. It tells the OS: "Let me know when data arrives on this socket." The OS handles this efficiently at the kernel level.

  • OS Blocking Operations: When the OS doesn't support an async version of a task (like some file system calls), Libuv’s Thread Pool steps in to manage the waiting.


DNS: The "Secret" Blocker

If you've ever wondered why your high-performance Node app suddenly lags when making outgoing API calls, the culprit is likely how libuv handles dns.lookup().

In Node.js, there are two ways to handle DNS, and they behave very differently under the hood.

1. dns.lookup( ) (The Default Blocker): When you make an HTTP request (like axios.get('example.com')), Node.js needs to find the IP address for example.com. By default, it uses a function called dns.lookup().

  • How it looks: It uses a callback or a promise, so it seems non-blocking.

  • The Reality: Under the hood, dns.lookup() uses a system call called getaddrinfo. This system call is synchronous and blocking meaning it stops everything until it gets an answer from the DNS server.

The Result: if you make 100 concurrent API requests to different domains, and your thread pool is the default size of 4, the 5th request has to wait for one of the first 4 DNS lookups to finish. Your entire application's outgoing throughput is throttled by 4 threads.

2 . dns.resolve( ) (The Truly Async Way): The Mechanism: This uses c-ares, a C library specifically for asynchronous DNS requests.

  • The Benefit: It performs the network communication for DNS directly through the Event Loop without touching the Thread Pool.

  • The Catch: It does not use the local /etc/hosts file or OS-specific configuration. It talks directly to DNS servers.

  • It goes straight to the network (the DNS servers configured in your /etc/resolv.conf).


3 . The Node.js C++ Binding Layer

The C++ Binding Layer is the essential bridge that allows JavaScript to communicate with the low-level C++ systems that actually power Node.js.

Since JavaScript is traditionally a high-level language meant for the browser, it cannot directly access your computer's hardware, file system, or network sockets. The Binding Layer "wraps" these complex C++ operations into clean JavaScript functions that we can call in our code.

Bindings: you probably have noticed by this time that Node.js is written in both JavaScript and C/C++. The reason that there are so many C/C++ code/libraries is simple: they are fast. However, how is it possible that the code you write in JavaScript end up communicating smoothly with code written in C/C++?

Aren’t they three difference programming languages? Yes they are. And normally code written in different languages cannot be communicate with each other. Not without bindings.

Bindings, as the name implies, are glue codes that “bind” one language with another so that they can talk with each other. In this case (Node.js), bindings simply expose core Node.js internal libraries written in C/C++ (c-ares, zlib, OpenSSL, http-parser, etc.) to JavaScript.

One motivation behind writing bindings is code reuse: if a desired functionality is already implemented, why write the entire thing again, just because they are in different languages? Why not just bridge them? Another motivation is performance: system programming languages such as C/C++ are generally much faster than other high-level languages (e.g. Python, JavaScript, Ruby, etc.). Therefore it might be wise to designate CPU-intensive operations to codes written in C/C++, for example.

C/C++ Addons: Bindings only provide glue code for Node.js’ core internal libraries, i.e. zlib, OpenSSL, c-ares, http-parser, etc. If you want to include a third-party or your own C/C++ library in your application, you would have to write the glue code for that library yourself. These glue code you write are called addons. Think of bindings and addons as bridges between your JavaScript code and Node.js’ C/C++ code.


The "Single-Threaded" Myth vs. Reality

It’s one of those classic interview questions that people tend to answer with a half-truth: "Node.js is single-threaded."

But reality is different,

The "Single-Threaded" Part

Your JavaScript code (the logic you write) runs on exactly one thread the Event Loop. This is the "manager" that coordinates tasks. If this thread gets stuck doing a massive calculation, your entire app freezes because the manager is busy.

The "Multi-Threaded" Part

While the manager is single-threaded, they have a massive back-office team (Libuv) and contractors (the OS Kernel) doing the heavy lifting in the background:

  • Libuv Thread Pool: By default, Node spawns 4 background threads to handle "heavy" tasks like reading files, database encryption, and complex math.

  • The OS Kernel: For network requests (HTTP), Node offloads the work to the Operating System, which can handle thousands of connections simultaneously without using Node's threads at all.

  • Worker Threads: Modern Node.js allows you to manually create your own "mini-managers" (threads) to run heavy JS logic in parallel.


Real-World Implementation

Advanced Architectural Patterns

The Top 5 Patterns:

  1. Clean Architecture: Separates your business logic (the "brain") from your tools (DB, Express, APIs). This allows you to swap databases or frameworks without rewriting your core logic.

  2. CQRS (Command Query Responsibility Segregation): Splits the code that writes data (Commands) from the code that reads data (Queries). This optimizes performance for read-heavy apps.

  3. BFF (Backend-for-Frontend): Creates a unique "mini-API" for different devices (Mobile vs. Web) so each client gets exactly the data it needs no more, no less.

  4. Event-Driven / Microservices: Uses a Message Broker (like RabbitMQ) so services talk to each other through "events" instead of direct calls. This prevents one failing service from crashing the whole system.

  5. Streams/Pipelines: Processes massive amounts of data in tiny "chunks." This keeps memory usage low, even when handling gigabytes of files or logs.

When Node's Architecture Shines (I/O Heavy) vs. Where it Struggles (CPU Heavy)

Architecture Comparison: I/O vs. CPU

Feature

I/O-Heavy Workloads (The "Sweet Spot")

CPU-Heavy Workloads (The "Bottleneck")

Definition

Tasks spent waiting for external resources (Network, Database, File System).

Tasks requiring intense local computation and logic.

Mechanism

Uses Non-blocking I/O. The Event Loop offloads the "wait" to the OS/Worker threads and moves to the next task.

Tasks stay on the Main Thread. The Event Loop cannot move until the calculation is finished.

Concurrency

High. Can handle thousands of concurrent connections with minimal memory overhead.

Low. One heavy calculation can "freeze" the server for all other users.

Examples

Chat apps, Streaming, REST APIs, Real-time collaboration tools (Slack/Trello).

Video encoding, Image processing, Heavy cryptography, Data crunching.

Performance

Excellent efficiency; low latency for high-volume requests.

Poor; leads to "Event Loop Blockage" where the server becomes unresponsive.


Conclusion

Node.js isn't just a runtime; it’s a carefully choreographed dance between the V8 engine, Libuv, and the Event Loop. By understanding that "single-threaded" only applies to your code not the underlying system, you can build applications that handle thousands of users with a fraction of the resources traditional servers require.

Node.js shines as a high-performance, I/O‑oriented platform designed for real‑time, connection‑heavy applications. Its single‑threaded event loop and non‑blocking I/O model make it ideal for scenarios like group chats, live feeds, and other workloads that require handling thousands of small, concurrent requests with low latency. It is not suited for CPU‑bound "heavy lifting" (e.g., large media processing) without offloading that work to worker threads, separate services, or native modules.

T
Taylor27563mo ago

Love this kind of content.

D

thank you for appreciation