Phase 1 · Chapter 1
The Computer Before the Code
Processes, Threads, Memory, and the Node.js Event Loop
What this chapter does to you: After this, when someone says "Node.js is single-threaded," you will not nod vaguely. You will be able to draw exactly what is happening inside the machine — down to memory. LLD/HLD interviews start here. System Design starts here. Everything starts here.
Part 1: The Computer is Just a Very Obedient Idiot
Before we talk about servers, we need to talk about what a computer actually is.
A computer has exactly three things that matter to us:
- CPU — the brain. It executes one instruction at a time. Addition, comparison, jump to this memory address. That's it. It does nothing else.
- RAM (Memory) — the scratchpad. Fast, temporary. When your server is running, everything lives here.
- Disk — the warehouse. Slow, permanent. Your database files, your code files, everything sits here.
The CPU is insanely fast. We're talking billions of instructions per second. Disk is insanely slow compared to CPU. Reading from disk is like the CPU sending a request to another city and waiting for a reply. RAM sits in between.
This speed difference — CPU vs Disk — is the single most important physical fact in all of software engineering. Almost every architectural decision you will ever make traces back to this one fact.
Keep this in your head always:
CPU speed : ~3,000,000,000 operations/second
RAM access : ~100 nanoseconds (CPU waits ~300 cycles)
SSD read : ~100 microseconds (CPU waits ~300,000 cycles)
Network request : ~100 milliseconds (CPU waits ~300,000,000 cycles)
Read that last line again. A network request — your API calling a database — makes the CPU wait 300 million cycles. Every time.
Part 2: What is a Process?
When you run your Node.js server — node server.js — the operating system (Linux, in your case on Hetzner) does something very specific.
It creates a Process.
Think of a process as a completely isolated apartment.
The operating system gives this apartment:
- Its own chunk of RAM (no other process can see inside)
- Its own file handles (which files it has opened)
- A process ID (PID) — the apartment number
- At least one CPU thread to do work (we'll get to threads)
The isolation is the key property. If your Node.js server crashes, it doesn't corrupt Postgres. Because they're in separate apartments. The OS is the building manager — it enforces walls between tenants.
When you run pm2 start server.js, PM2 is just a babysitter that watches your apartment and restarts it if it collapses.
The vocabulary:
Process= isolated program in execution, with its own memory spacePID= process identifier, the number the OS assigns itfork()= the system call that creates a child process (a copy of the parent)
The physical reality: When your Node.js process starts, the OS allocates a contiguous region of RAM for it. This region is divided into sections — we'll see which ones in a moment.
Part 3: What is Memory? (Stack vs Heap)
Inside your process's RAM allocation, memory is divided into two very different regions. Most engineers treat these as vocabulary words. We are going to understand them physically.
The Stack
Imagine a physical stack of trays in a cafeteria.
When your code calls a function, the OS pushes a new tray on the stack. This tray holds:
- The function's local variables
- The return address (where to go back after the function finishes)
When the function returns, the tray is popped off. Gone. Instantly.
function add(a, b) {
let result = a + b; // 'result' lives on the stack
return result;
} // tray is popped, 'result' ceases to exist
Properties of the Stack:
- Extremely fast — push and pop are single CPU instructions
- Size-limited — typically 8MB per thread. Stack overflow = you ran out of trays
- Automatically managed — no cleanup needed, the tray disappears on return
- LIFO order — Last In, First Out. Innermost function always on top
The Heap
The Heap is everything else. It's the open warehouse floor.
When you do const user = { name: "Trilok", age: 28 } in JavaScript, that object doesn't fit neatly on a stack tray. It's a complex structure that might grow, might be referenced from many places, might outlive the function that created it.
So it goes on the Heap.
function createUser(name) {
const user = { name: name, scores: [] }; // 'user' object lives on the HEAP
return user; // we return a REFERENCE to it
}
// The function returns, stack tray is popped
// But the object on the heap stays until nobody references it
Properties of the Heap:
- Flexible — any size, any shape, lives as long as needed
- Slower — allocation requires bookkeeping (finding free space)
- Manually managed in languages like C (you must free it)
- Garbage Collected in JavaScript — Node.js has a GC that scans for objects nobody references anymore and reclaims the memory
The memory leak you've probably hit: You create objects in the Heap, store references to them in a global array or a closure, and forget about them. GC can never collect them. Over hours, your Node.js RAM usage climbs. Server slows. Crash. That's a heap memory leak.
Why this matters in interviews: When someone asks "what's the difference between stack and heap?" — most candidates say "stack is fast, heap is for objects." You will say: the stack is a LIFO tray system per function call frame, automatically reclaimed on return, size-bounded. The heap is the free warehouse, dynamically allocated, requires garbage collection or manual management. The cost difference exists because stack allocation is a single pointer move while heap allocation requires finding contiguous free space and updating bookkeeping structures.
Part 4: What is a Thread?
A process is the apartment. A Thread is the worker inside the apartment.
One process can have many threads. Each thread is an independent sequence of instructions, but all threads inside the same process share the same heap memory.
This sharing is powerful. And dangerous.
Powerful: Thread A can put data in a shared object on the heap. Thread B can read it instantly. No copying, no network, just RAM.
Dangerous: If Thread A and Thread B try to write to the same memory location at the exact same time — you get race conditions. The data gets corrupted. The bug is non-deterministic. It might happen once every thousand requests. These bugs are nightmares to debug.
The Traditional Server Model (Apache, Java)
Before Node.js existed, here's how web servers worked:
The problem: threads are expensive.
Each thread needs:
- Its own Stack (typically 1-8MB of RAM)
- OS bookkeeping overhead
- Context switching cost — when the CPU switches between threads, it must save the current thread's state and restore the next thread's state. This takes microseconds. At scale, it dominates.
A server with 1,000 concurrent connections = 1,000 threads = 1-8GB of RAM just for stacks + enormous context switching overhead. This was called the C10K Problem (handling 10,000 concurrent connections). In the early 2000s it was unsolved.
The key word: Blocking I/O. A thread that's waiting for a DB response is blocked. It's consuming memory and OS resources but doing zero actual work. It's a worker asleep at their desk while the other desks are full.
Part 5: Node.js — The Single-Threaded Superpower
Ryan Dahl created Node.js in 2009 to solve exactly this problem.
His insight: most of what a web server does is waiting, not computing.
- Waiting for a DB query
- Waiting for a file to be read
- Waiting for an external API
If almost all the time is spent waiting, why dedicate a whole thread per request? What if one thread handled all the waiting?
That's Node.js.
Node.js is single-threaded. It has exactly one main thread of JavaScript execution.
But it handles thousands of concurrent requests. How?
Part 6: The Event Loop — The Most Important Diagram in Your Career
This is the mechanism. Understand this and you understand Node.js completely.
The loop works like this — in plain English:
- Check if the Call Stack has work. If yes, run it.
- If the stack is empty, check the Event Queue.
- If the queue has a completed callback, move it to the stack and run it.
- Repeat forever.
What LIBUV is doing under the hood:
When your JavaScript says db.query(sql, callback), the Node.js C++ layer (libuv) takes that database call and gives it to the OS. The OS has mechanisms (on Linux: epoll) to watch thousands of file descriptors (network connections, files) simultaneously without blocking. The OS notifies libuv when data arrives. Libuv then puts your callback into the Event Queue. The Event Loop picks it up and runs it.
The single JavaScript thread never blocks. It fires off I/O requests and moves on. When results come back, it handles them.
The Concrete Flow with 500 Students
Now let's revisit your AttemptIQ scenario with real understanding:
This is why Node.js destroyed the C10K problem. It doesn't use one thread per connection. It uses one thread per event. Events are cheap. Callbacks are just function pointers.
Part 7: Where Node.js Does Break (The Things That Will Bite You)
Node.js is NOT magic. The single-thread model has one hard constraint:
If your JavaScript code does heavy computation, it blocks the entire event loop.
No I/O waiting, no callbacks processed, no new requests handled. Everything queues up and waits.
Example — this breaks everything:
app.get('/analyze', (req, res) => {
// This is a CPU-intensive operation
let result = 0;
for (let i = 0; i < 10_000_000_000; i++) {
result += Math.sqrt(i); // 10 billion iterations
}
res.send({ result });
});
While this loop runs:
- All other HTTP requests are frozen
- No BullMQ callbacks execute
- No DB responses are processed
- Your entire server is a brick
This is called blocking the event loop. It is the cardinal sin of Node.js.
How to fix it:
- Worker Threads — Node has
worker_threadsmodule. Spin off CPU work to a separate thread. - Offload to a queue — put the heavy computation in a BullMQ worker (a separate process). This is exactly what you're already doing with AttemptIQ analytics. You instinctively solved this problem correctly.
- Use a different language for computation — Python/Go services for ML inference, etc.
The other thing that bites people: async/await mistakes
// WRONG — this blocks
app.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users'); // fine
const processed = users.map(u => heavyTransform(u)); // blocks! map is sync
res.send(processed);
});
await only helps with I/O. If heavyTransform is CPU work, await does nothing — it still blocks.
Part 8: The Vocabulary Map — What You Now Know
Every word connects to the physical reality you just learned.
| Word | What it actually is |
|---|---|
| Process | Isolated apartment in RAM with its own memory space |
| Thread | A worker inside the process, with its own stack |
| Stack | Per-thread LIFO memory for function calls. Fast, limited, auto-managed |
| Heap | Shared RAM warehouse for objects. GC-managed in JS |
| Blocking I/O | Thread sits idle waiting for I/O. Wastes CPU cycles |
| Non-blocking I/O | Thread fires I/O request and continues. OS notifies on completion |
| Event Loop | The mechanism that routes completed I/O callbacks to JS execution |
| libuv | The C++ layer that wraps OS async mechanisms (epoll on Linux) |
| epoll | Linux OS mechanism: watch thousands of file descriptors without blocking |
| Concurrency | Multiple things in progress at once (NOT the same as parallel) |
| Parallelism | Multiple things executing at the exact same instant (requires multiple CPU cores) |
| Race Condition | Two threads writing to shared memory simultaneously, corrupting it |
| Context Switch | CPU saving one thread's state and loading another's. Has a cost |
| Memory Leak | Heap objects with live references that are never used — GC can't collect them |
Concurrency vs Parallelism — one more time because interviews love this:
Node.js is concurrent but not parallel (in the JS layer). Multiple requests are "in progress" at the same time — but not executing JavaScript at the same time. They take turns. The I/O happens in parallel (via libuv and OS threads) but JS runs one callback at a time.
Go, Java, Python with threading — those can be both concurrent AND parallel (JS runs on multiple cores at the same time).
Part 9: The Practice Problems
Don't skip these. These are where the understanding becomes weaponized.
Problem 1 (Reasoning): Your AttemptIQ API has an endpoint that generates a PDF report for each student. The PDF generation is CPU-intensive (parsing data, rendering charts). You notice that when 10 students request their PDF at the same time, ALL requests slow down — not just those 10, but every other API endpoint too.
Why is this happening? What is the physical explanation using what you learned? What are the two ways to fix it?
Problem 2 (Reasoning):
A junior developer on your team says: "I used async/await everywhere, so my code is definitely non-blocking."
Are they right? Explain with a concrete counterexample.
Problem 3 (Architecture): You have a Node.js API. You want to send a "welcome email" to every new user who signs up. Sending email takes ~2 seconds (external SMTP server).
Option A: await emailService.send(user.email) inside the signup handler.
Option B: Push a job to BullMQ and return the response immediately.
Explain exactly what happens to your server under load for each option. What is the maximum number of concurrent signups Option A can handle before things go bad?
Problem 4 (Go Deeper):
You read that libuv uses a "thread pool" internally (default size: 4 threads). But we said Node.js is single-threaded.
These two facts seem contradictory. Resolve the contradiction. (Hint: which operations use the thread pool? Which use epoll directly?)
What's Coming in Chapter 2
You now understand what happens inside one machine when a request arrives.
Chapter 2 is about what happens between machines — the actual physical journey of a request from a browser in India to your server in Germany.
We'll cover:
- TCP: why your API is reliable (and what that reliability costs)
- UDP: why video calls use it instead
- The actual HTTP request lifecycle — byte by byte
- DNS: how
attemptiq.combecomes138.68.x.x - TLS: why HTTPS doesn't slow you down as much as you think
After Chapter 2, you will know what happens at every layer between a student's click and your database query. That knowledge makes system design diagrams trivially easy — you're not guessing, you're remembering physics.
End of Phase 1, Chapter 1. Estimated reading time: 45-60 minutes. Practice problems: 30-60 minutes. Do not proceed to Chapter 2 until you can answer all four practice problems from memory.