The Node.js Event Loop Explained

Search for a command to run...

No comments yet. Be the first to comment.
You open WhatsApp, type a message, and hit send. A single grey tick appears. You're on a train with no signal, but the message went through anyway. Or did it? That single tick is the beginning of a su

You record a Reel, add a song, trim it, and then close the app before posting. When you reopen Instagram, the draft is right there waiting. Nothing was lost. That experience feels simple. The system b

If you have built a React Native app, you have felt the pain of navigation setup. It is easy to get wrong and hard to keep clean as apps grow. This article explains what routing means, why it matters,

Modern apps are not just a pile of screens. They are a living system of features, data flows, offline behavior, and performance constraints. If you have only shipped small apps, the jump to something

Your application has user accounts. Some routes should only be accessible to logged-in users. How do you protect routes and verify that a request actually comes from who they claim to be? JWT (JSON We

Ashish's Blog
57 posts
The event loop is the heart of Node.js. It's what makes Node.js single-threaded yet capable of handling thousands of concurrent requests. Understanding it is crucial to writing efficient Node.js applications.
Node.js runs on a single thread. There's one thread executing your JavaScript code. But a single thread would be useless if it blocked on every slow operation.
The event loop is the solution. It lets you execute code non-blocking. While waiting for operations to complete, the event loop checks what's ready and executes callbacks.
Imagine a task manager:
You give the manager tasks
The manager executes tasks one by one
For tasks that take time (like filing documents), the manager delegates to workers
Once workers finish, the manager executes the callback
The manager never sits idle waiting
This is the event loop.
When you run Node.js:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 100);
console.log('End');
Output:
Start
End
Timeout
Here's what happens behind the scenes:
1. Main JavaScript code executes
- Logs "Start"
- setTimeout schedules callback (doesn't execute now)
- Logs "End"
- Main code complete
2. Event loop checks: Is anything ready?
- 100ms haven't passed, nothing ready
3. 100ms passes
4. Event loop checks: Is anything ready?
- Timeout callback is ready! Execute it
- Logs "Timeout"
5. No more callbacks, event loop waits
The event loop is constantly checking: "Is there anything I need to do?"
The event loop manages two things:
Call Stack: Where your code executes
Callback Queue: Where callbacks wait to execute
Your Code
|
v
Call Stack (currently executing)
|
v (if done)
Event Loop (checks: anything in queue?)
|
v
Callback Queue (waiting callbacks)
When the call stack is empty, the event loop picks a callback from the queue and executes it.
Here's a more detailed flow:
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
Timeline:
Time 0ms:
- Call Stack: [console.log('1')]
- Execute: Logs '1'
- Call Stack is empty
Time 0ms:
- Call Stack: [setTimeout callback scheduled]
- Callback added to queue
- Call Stack is empty
Time 0ms:
- Call Stack: [console.log('3')]
- Execute: Logs '3'
- Call Stack is empty
Time 0ms (main code done):
- Event Loop checks queue
- Callback queue has the setTimeout callback
- Move callback to call stack
- Execute: Logs '2'
- Call Stack is empty
Done
Output:
1
3
2
Even though setTimeout had 0ms delay, it doesn't execute immediately. It's always after the main code finishes.
The event loop has several phases. Each phase handles different types of operations:
┌───────────────────────────┐
│ Event Loop Phases │
├───────────────────────────┤
│ 1. Timers │ (setTimeout, setInterval)
│ 2. Pending Callbacks │ (OS operations)
│ 3. Idle, Prepare │ (Internal)
│ 4. Poll │ (I/O operations)
│ 5. Check │ (setImmediate)
│ 6. Close Callbacks │ (Socket close)
└───────────────────────────┘
The event loop cycles through these phases repeatedly.
File operations demonstrate the event loop perfectly:
import fs from 'fs';
console.log('Start');
fs.readFile('file.txt', (err, data) => {
console.log('File read');
});
console.log('End');
Output:
Start
End
File read
Timeline:
1. Main code starts
2. Logs "Start"
3. fs.readFile called - work delegated to file system worker
4. Logs "End"
5. Main code done
6. Event loop checks: Is file read done?
7. No, not yet
8. ...time passes...
9. Event loop checks: Is file read done?
10. Yes! Add callback to queue
11. Event loop executes callback from queue
12. Logs "File read"
Your code never waits. The file is read in the background while your code continues.
Both defer execution, but they're in different phases:
setImmediate(() => console.log('Immediate'));
setTimeout(() => console.log('Timeout'), 0);
Output (usually):
Timeout
Immediate
setTimeout executes in the Timers phase, setImmediate in the Check phase. The event loop visits Timers first.
There's a subtle complexity: microtasks and macrotasks.
Microtasks (execute immediately after each operation):
Promises (.then, .catch, .finally)
process.nextTick()
Macrotasks (queued in phases):
setTimeout
setInterval
setImmediate
I/O operations
Here's the key: After each macrotask, the event loop executes ALL microtasks before moving to the next macrotask.
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
console.log('End');
Output:
Start
End
Promise 1
Promise 2
Timeout
Timeline:
1. Main code: Logs "Start"
2. Main code: setTimeout scheduled (to Macrotask queue)
3. Main code: Promises created and scheduled (to Microtask queue)
4. Main code: Logs "End"
5. Main code done
6. Event loop checks: Any microtasks?
7. Yes! Execute Promise .then
8. Logs "Promise 1"
9. Microtask queue still has one
10. Execute it
11. Logs "Promise 2"
12. No more microtasks
13. Event loop checks: Any macrotasks?
14. Yes! setTimeout callback
15. Execute it
16. Logs "Timeout"
Promises always execute before setTimeout, even with 0ms delay!
Here's a realistic scenario:
import fs from 'fs/promises';
console.log('1: Start');
setTimeout(() => {
console.log('2: setTimeout');
}, 0);
fs.readFile('file.txt', 'utf8')
.then(data => {
console.log('3: File read (Promise)');
});
Promise.resolve()
.then(() => console.log('4: Promise'));
console.log('5: End');
Output:
1: Start
5: End
4: Promise
3: File read (Promise)
2: setTimeout
Explanation:
Synchronous code runs first (1, 5)
Microtasks run (4)
File read completes and resolves (3) - still a microtask
Macrotasks run (2)
Mistake 1: Assuming setTimeout runs immediately
setTimeout(() => {
console.log('runs second');
}, 0);
console.log('runs first');
Even with 0ms, setTimeout callback doesn't run immediately. It waits until the main code finishes.
Mistake 2: Long-running operations block the event loop
// Bad - blocks event loop for 5 seconds
function expensive() {
for (let i = 0; i < 1_000_000_000; i++) {
Math.sqrt(i);
}
}
setTimeout(() => {
expensive(); // Blocks entire event loop
}, 100);
// While expensive() runs, no other callbacks execute
Mistake 3: Thinking setImmediate is faster
setImmediate(() => console.log('1'));
setTimeout(() => console.log('2'), 0);
setImmediate doesn't necessarily run first. It depends on whether the current phase is the Check phase.
You can visualize event loop behavior:
const start = Date.now();
setTimeout(() => {
console.log(`Timer: ${Date.now() - start}ms`);
}, 100);
Promise.resolve()
.then(() => console.log(`Promise 1: ${Date.now() - start}ms`))
.then(() => console.log(`Promise 2: ${Date.now() - start}ms`));
console.log(`Main: ${Date.now() - start}ms`);
Output:
Main: 0ms
Promise 1: 0ms
Promise 2: 0ms
Timer: 102ms
The promises run immediately (microtasks), the timer waits for its delay.
The event loop continuously checks what work is ready to do
Your JavaScript code runs on the main thread, blocking the event loop
I/O operations run on background workers, not blocking the event loop
Callbacks wait in queues until the event loop picks them up
Microtasks (Promises) always run before macrotasks (timers, I/O)
Even setTimeout(..., 0) doesn't run immediately
Long-running code blocks the entire event loop
Understanding the event loop is key to writing efficient Node.js
The event loop isn't magic. It's a simple idea: check what's ready, execute it, repeat. Master this concept, and you'll write Node.js code that scales and performs well.