30 July 2025
Michael McAndrew created group «NodeJS Event Loop» with members Michael McAndrew and LLM-Dev
MM
12:42
Michael McAndrew
MM
12:44
Michael McAndrew
/set_model anthropic/claude-sonnet-4-0
L
12:44
LLM-Dev
In reply to this message
Your model id has been changed to: anthropic/claude-sonnet-4-0
MM
12:44
Michael McAndrew
I'm trying to understand more about NodeJS internals, specifically how the event loop works in Node. Could you help me to learn about how it works.
L
12:44
LLM-Dev
In reply to this message
Great topic! Understanding the event loop is key to really grasping how Node.js works under the hood.

Before we dive in, let me get a sense of where you're starting from - what's your current experience with Node.js? Have you built applications with it before, or are you newer to the platform?

Also, when you think about JavaScript running in the browser versus Node.js, what do you already know about how JavaScript handles things like setTimeout or file operations?
MM
12:46
Michael McAndrew
Yes I have written applications in Node before, but I wouldn't say I've built massive applications at scale with it. In general, I'm familiar with the idea of callbacks, and async I/O, and I/O at a high level, but I'm rusty on the details at the os level
L
12:46
LLM-Dev
In reply to this message
Perfect! Since you've got the high-level async concepts down, we can dig into the mechanics.

Let's start with a foundation question: When you run a Node.js application, there's actually more than just the JavaScript engine (V8) doing work. What do you think are the main components that make up a running Node.js process?

Think about what needs to handle things like file system operations, network requests, timers, etc. - since JavaScript itself is single-threaded, something else must be managing all that concurrent I/O work.
MM
12:51
Michael McAndrew
I'm not entirely sure to be honest. I know that the OS manages concurrent I/O work and has some kind of way of "signalling" when it is completed. I can see an event loop that manages what "Task" the current thread can run, and where tasks can yield control of the thread to the event loop when they're waiting on I/O to complete. I'm guessing that's what's happening here. Where I'm fuzzy is on how the event loop knows when I/O has finished, and when it should interrupt a running task to handle it with a callback for example?
L
12:51
LLM-Dev
In reply to this message
Excellent intuition! You're absolutely right about the OS signaling when I/O completes, and you've identified the key question: how does the event loop know when to act on those signals?

Here's the missing piece: Node.js uses a library called libuv that sits between your JavaScript code and the operating system. Think of it as the translator and coordinator.

So when you do something like fs.readFile(), what path do you think that request takes?

Start with your JavaScript code calling fs.readFile() - where does it go next, and how do you think it eventually gets to the OS to do the actual file reading?
MM
13:21
Michael McAndrew
Ok, so I'd say the runtime would send some kind of request to the OS to say "Hello, I'd like to read this file". The OS would respond saying "Ok, I'm working on it". At that point the code would say "I'm waiting on this file reading thing from the OS, I'm happy to yield the thread to some other task that needs to do work". It then yields control to some code that manages what tasks are running on the thread, and it adds the task to some kind of queue to keep track of running tasks. Then, at some stage the OS will say "I'm finished with that file reading thing now", and the next time the thread is yielded, the event loop controlling code will decide to run the task that depended on the file read in order to continue, and execute its callback function. A bit hand wavey :)
L
13:21
LLM-Dev
In reply to this message
That's a really solid mental model! You've got the key flow right - the yielding, the queuing, and the OS signaling completion.

Let me help you fill in one crucial detail: you mentioned the OS says "I'm finished" - but how does it actually tell the event loop that?

The OS can't just randomly interrupt your JavaScript code. Instead, the event loop has to actively ask the OS "hey, is anything ready yet?"

This is where libuv comes in with something called polling. What do you think happens during this polling step? How might the event loop check with the OS for completed I/O operations?
MM
13:23
Michael McAndrew
Oh so libuv is a program that will literally check some memory addresses periodically to see if the I/O task has completed? Those kinds of things don't use interrupts?
L
13:23
LLM-Dev
In reply to this message
You're on the right track with the checking idea, but there's a more elegant solution than periodically checking memory addresses!

Think about it - if libuv had to constantly check "is it done yet? is it done yet?" that would waste a lot of CPU cycles, right?

Instead, most operating systems provide a mechanism where the program can say: "Hey OS, I'm interested in these I/O operations. Wake me up when ANY of them complete, but until then, let me sleep."

Have you heard of system calls like epoll on Linux or kqueue on macOS? If not, what do you think the advantage would be of having the OS wake up your program only when something is actually ready, versus constantly checking?
MM
13:24
Michael McAndrew
no
L
13:24
LLM-Dev
In reply to this message
No worries! Let me explain this key concept.

Instead of constantly checking (which would waste CPU), the OS provides special system calls that work like this:

1. Your program gives the OS a list: "I'm waiting for these file reads, network requests, etc."
2. Your program says: "Put me to sleep until ANY of these complete"
3. The OS literally pauses your program (no CPU usage!)
4. When I/O finishes, the OS wakes up your program and says "here's what completed"

This is called event-driven I/O - the OS notifies your program of events, rather than your program constantly asking.

So when Node.js appears to be "doing nothing" (like waiting for a database query), what do you think is actually happening to the Node process at the OS level?
MM
13:26
Michael McAndrew
Well it could be executing some other tasks off the task queue, or it could just have yielded execution to some other processes running on the system for the OS?
L
13:26
LLM-Dev
In reply to this message
Exactly right on the second part! When Node has no JavaScript tasks ready to run and is waiting for I/O, the entire Node process gets put to sleep by the OS. The CPU can then work on completely different programs.

But let me clarify something about the first part - when you say "other tasks off the task queue," what do you mean by that?

Here's a key point: if ALL of your JavaScript is waiting for I/O (like database queries, file reads, etc.), then there actually aren't any JavaScript tasks ready to run. The event loop would have nothing to execute.

Can you think of a scenario where Node.js would have JavaScript work to do while also waiting for I/O operations to complete?
MM
13:32
Michael McAndrew
Sure yeah so if you write some code that has to wait on reading files, it can still go on and execute any other synchronous (or Async) code in the program until it has nothing to do anymore. Of course, if it has done all of that, then it can yield back to the OS while it waits to do things
L
13:32
LLM-Dev
In reply to this message
Perfect! You've got it. So the event loop is constantly making this decision: "Do I have any JavaScript ready to execute? If yes, run it. If no, ask the OS to wake me when I/O completes."

Now here's where it gets interesting - not all async operations in Node work exactly the same way.

You mentioned file operations, which do use that OS event notification system we talked about. But what about something like setTimeout(callback, 1000) or CPU-intensive tasks like cryptographic operations?

Think about it: can the OS help with a timer that should fire in exactly 1 second? What about a complex calculation that just takes time but doesn't involve I/O?

How do you think the event loop handles these different types of async work?
MM
13:34
Michael McAndrew
Well I guess a CPU-intensive task will have to run synchronously because it needs to run on the CPU thread that the node process manages?
L
13:34
LLM-Dev
In reply to this message
Good insight! Yes, CPU-intensive JavaScript code is synchronous and will block the main thread.

But here's where Node gets clever: for certain CPU-intensive operations that Node provides (like crypto functions, file system operations, DNS lookups), it doesn't run them on the main JavaScript thread at all.

Instead, Node.js actually uses a thread pool - a separate set of worker threads that can handle this heavy work in the background.

So when you call something like crypto.pbkdf2() (a CPU-intensive hashing function), what do you think happens? Does it block your main JavaScript thread, or is there another way Node handles it?

And as a follow-up: how would this thread pool fit into that event loop flow we've been discussing?
MM
13:37
Michael McAndrew
Well yeah you've just told me that it has another thread pool for running those kinds of things. I'm guessing it treats that kind of async work in a similar way to waiting I/O.
How does the crypto library get to use the thread pool, is there some kind of Node API that it can use to do it?
L
13:40
LLM-Dev
In reply to this message
Exactly right! Node treats thread pool work just like I/O - the main thread doesn't block, and when the work finishes, it gets queued up for the event loop to handle.

As for your API question - yes, there is! It's actually libuv again. Libuv provides both the I/O polling we talked about AND the thread pool functionality.

So when a Node.js module (like crypto) wants to offload work to the thread pool, it uses libuv's thread pool API. Libuv handles creating the threads, managing the work queue, and notifying the main thread when work completes.

Here's an interesting question: what do you think determines which operations use the thread pool versus which ones use the I/O polling mechanism?

Think about the difference between reading a file and doing a cryptographic hash calculation. What's fundamentally different about how these operations work?
MM
13:44
Michael McAndrew
Well doing a cryptographic hash calculation is CPU-bound whereas a file read is I/O-bound. I don't know *how* NodeJS decides though, I thought maybe it would be explicitly set by the implementors of the function call?
L
13:44
LLM-Dev
In reply to this message
Exactly! You nailed it - it's explicitly decided by the implementors based on whether the operation is CPU-bound or I/O-bound.

The Node.js core team (and module authors) choose which mechanism to use when they write each function. File operations use I/O polling because the OS can notify when files are ready. Crypto operations use the thread pool because they need actual CPU cycles to compute.

Now let's put this all together. You've got:
- JavaScript code running on the main thread
- I/O operations managed by OS event notifications
- CPU-intensive operations running on a thread pool
- An event loop coordinating everything

Can you walk me through what happens when this code runs?

console.log('start');
setTimeout(() => console.log('timer'), 0);
fs.readFile('file.txt', () => console.log('file'));
console.log('end');


What order do you think these console.log statements will appear, and why?
MM
13:47
Michael McAndrew
Yeah sure, the order would probably be something like:
- "start"
- "timer"
- "end"
- "file"
because the timer is set to delay for 0 seconds
L
13:47
LLM-Dev
In reply to this message
Good reasoning on the first three! You got that the synchronous console.log statements run first, and that even a 0ms timer goes through the event loop.

But I'm curious about your ordering of "timer" vs "file" - you said timer comes before file because it's set to 0ms delay.

Here's the thing: that file read also starts immediately (no delay), and depending on the file size and system, it might actually complete faster than even a 0ms timer.

So what do you think determines the order when both the timer and file operation complete around the same time? Is there something about how the event loop processes different types of completed work?
MM
13:48
Michael McAndrew
Huh weird, are timers CPU-bound in Node or something?
L
13:48
LLM-Dev
In reply to this message
Great question! Timers aren't CPU-bound - they're actually quite lightweight. The event loop just tracks when each timer should fire.

The key insight here is that the event loop doesn't just grab completed work randomly. It actually processes different types of completed work in a specific order - it has phases.

Think about it: when the event loop wakes up and finds that both a timer and a file read have completed, it needs some way to decide what to handle first, right?

What do you think might be a logical way to organize this? If you had to design priorities for handling: timers, I/O callbacks, and other types of async work - how might you structure that?
MM
13:49
Michael McAndrew
Using a FIFO queue?
L
13:49
LLM-Dev
In reply to this message
FIFO makes sense for handling items within the same type of work, but here's the key insight: the event loop actually has multiple queues for different types of work!

So it's not just one FIFO queue with everything mixed together. Instead, imagine something like:
- A timer queue (for setTimeout, setInterval)
- An I/O queue (for file reads, network requests)
- Other specialized queues...

The event loop processes these queues in a specific order each cycle. It might say "first, handle any expired timers, then handle any completed I/O, then..."

Given that structure, what do you think happens in our example? If the event loop checks the timer queue before the I/O queue each cycle, what would that mean for the order of "timer" vs "file"?
MM
13:50
Michael McAndrew
But is a timer not I/O?
L
13:50
LLM-Dev
In reply to this message
Excellent question! That's actually a really insightful point to raise.

Timers could be considered a form of I/O in the abstract sense - after all, you're asking the system to notify you when time has passed. But in Node's implementation, they're handled differently.

Think about it this way: what's the fundamental difference between asking the OS "tell me when this file is ready" versus asking "tell me when 100 milliseconds have passed"?

For file I/O, Node relies on the OS to manage the actual work and notify when complete. But for timers, what do you think Node itself needs to keep track of? What information would the event loop need to manage timers internally?
MM
13:53
Michael McAndrew
I'm not sure, but my thinking would be that you would want a timer callback to be handled with the highest priority because the callback logic could be time sensitive, whereas something like reading a file may not matter so much when it gets run
L
13:53
LLM-Dev
In reply to this message
That's really thoughtful reasoning about priorities! You're thinking like a system designer - time-sensitive operations should indeed get priority.

But here's an interesting twist: in Node's event loop, timers actually get checked first in each cycle, but there's a catch. Let me give you a concrete example:

setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));


Both of these are "timer-like" operations with no delay, but setImmediate is designed to run callbacks immediately after I/O events.

What do you think the output order would be here? And why might Node have created setImmediate if setTimeout(..., 0) already exists?

This will help us understand how Node balances the priority concerns you mentioned with other design considerations.
MM
13:54
Michael McAndrew
Ah so is the setTimeout kind of a "At least x amount of time has passed since this was triggered", whereas setImmediate is designed to be for more time sensitive callbacks? And in this case the callback for setImmediate is called before setTimeout?
L
13:55
LLM-Dev
In reply to this message
You're getting at something important! You're right that setTimeout is "at least X milliseconds" - it's a minimum delay, not exact timing.

But here's where it gets interesting: setImmediate isn't necessarily more time-sensitive. The name is a bit misleading!

The key difference is when in the event loop cycle each one gets processed. Think of the event loop as going through phases like:
1. Check timers (setTimeout, setInterval)
2. Handle I/O callbacks
3. Check setImmediate callbacks
4. Repeat...

So in our example:
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));


If both are ready to run, which phase runs first in the event loop cycle? What do you think that means for the output order?
MM
13:55
Michael McAndrew
- "immediate"
- "timer"
L
13:55
LLM-Dev
In reply to this message
Actually, it's the opposite! The output would be:
- "timer"
- "immediate"

Remember that phase order I mentioned:
1. Check timers first
2. Handle I/O callbacks
3. Check setImmediate callbacks

So even though setImmediate sounds like it should run immediately, it actually runs after timers in each event loop cycle.

Here's where it gets really interesting though: this ordering can actually change depending on the context! If this same code were inside an I/O callback (like inside a fs.readFile callback), the order would flip.

Why do you think the context matters? What's different about code that's already running as part of an I/O operation versus code that's running from the initial script?
MM
13:56
Michael McAndrew
Yeah I understand, it's because I/O callbacks are called before setImmediate
L
13:56
LLM-Dev
In reply to this message
Exactly! You've got the phases down.

But let me clarify the context part - it's not just that I/O callbacks run before setImmediate. It's about where the event loop is when these timers get scheduled.

When your script first runs, the event loop hasn't started its phases yet. So when it does start, it begins with phase 1 (timers) and finds your setTimeout.

But if you schedule a setTimeout and setImmediate from within an I/O callback, the event loop is already in the middle of phase 2. At that point, what do you think happens? Does it jump back to phase 1 to check timers, or does it continue forward to phase 3 (setImmediate) and catch the timer on the next full cycle?

This is what causes the ordering to flip in different contexts.
MM
14:02
Michael McAndrew
Ah right right, I see it will go onto phase 3
L
14:02
LLM-Dev
In reply to this message
Exactly! Once the event loop is in phase 2 (handling I/O callbacks), it continues forward to phase 3 (setImmediate) rather than jumping backward to phase 1 (timers).

So you've now got a solid understanding of the key pieces:
- libuv handling both I/O polling and thread pools
- The event loop processing different types of work in phases
- How context affects the ordering of async operations

Let me check your overall understanding: if I asked you to explain to someone else how Node.js can handle thousands of concurrent connections without creating thousands of threads, what would you tell them?

Try to weave together the main concepts we've covered - the single JavaScript thread, the event loop phases, and how I/O gets handled.
MM
14:04
Michael McAndrew
Node uses an event loop to manage what tasks have access to the main thread at any given time. For I/O-bound tasks like web server connections, most of the time is spent waiting. When a task needs to wait for something, Node will yield the thread, and pop the task callback onto an event queue, until the I/O task has come back with a response
L
14:04
LLM-Dev
In reply to this message
You've got the core idea! Let me help you refine a couple of key details:

You mentioned Node "pops the task callback onto an event queue" while waiting. Actually, what happens is Node registers the I/O operation with the OS (via libuv), and the callback stays in JavaScript memory. The event loop then goes to sleep until the OS signals that I/O is complete.

So it's not so much about managing thousands of callbacks in queues - it's that the OS can efficiently monitor thousands of I/O operations simultaneously, and wake up Node only when any of them complete.

Here's a test of your understanding: what's the key advantage of this approach versus creating a separate thread for each connection? Think about what happens when you have 10,000 idle connections that are just waiting for data.
MM
14:06
Michael McAndrew
If we're talking about CPU-threads, then it saves on CPU cycles
L
14:06
LLM-Dev
In reply to this message
You're on the right track! But let me help you think about this more specifically.

It's not just about CPU cycles - think about what happens with resources when you create 10,000 threads:

Each thread needs its own stack memory (typically several MB per thread). So 10,000 threads could easily use tens of gigabytes of RAM just for stacks, even if they're all just sitting idle!

Plus there's the overhead of context switching - the OS has to save and restore thread state when switching between thousands of threads.

With Node's approach, what do you think the memory footprint looks like when you have 10,000 idle connections? Remember, if they're all just waiting for I/O, what's actually consuming memory and CPU?