Back to all posts
·7 min read

Node.js Event Loop: The DJ That Never Stops Playing

Node.js is single-threaded — but somehow handles thousands of operations at once. Learn how the Event Loop, Call Stack, and task queues work together, explained through the lens of a DJ at a party.

Node.jsJavaScriptEvent LoopAsyncBeginners

Node.js Event Loop: The DJ That Never Stops Playing

Node.js is single-threaded. That means it can only do one thing at a time. And yet, it handles thousands of simultaneous connections without breaking a sweat. How?

The answer is the Event Loop — and understanding it will change how you think about asynchronous code forever.

Let's walk through it using a DJ party analogy. Stick with me.

The DJ's Table (Call Stack)

Picture a DJ at a party. The DJ's table (the Call Stack) can only play one song at a time. That's it. No matter how many requests come in, the DJ processes them one by one, in order.

In JavaScript terms, this is the call stack — a LIFO (Last In, First Out) structure where functions are pushed on when called and popped off when they return.

function greetGuest(name) {
  console.log(`Welcome, ${name}!`);
}

function openParty() {
  greetGuest("Alice");
  greetGuest("Bob");
}

openParty();
// Output:
// Welcome, Alice!
// Welcome, Bob!

Each function call gets stacked, executed, and removed before the next one runs. Simple, sequential, predictable.

The Roadies (Node Internals / libuv)

Now, the DJ can't leave the table to go set up the fog machine. That's the roadies' job.

In Node.js, the roadies are Node's internals (libuv and the OS async layer — equivalent to what browsers call Web APIs): setTimeout, fetch, file system reads, network requests. When you call one of these, Node hands the task off to the environment and immediately continues — the DJ keeps spinning tracks while the roadies do their thing.

console.log("Party starts");

setTimeout(() => {
  console.log("Fog machine ready!");
}, 2000);

console.log("DJ is already playing");

// Output:
// Party starts
// DJ is already playing
// Fog machine ready! (2 seconds later)

Notice: Node didn't stop and wait 2 seconds. It delegated to the roadie and moved on. That's non-blocking I/O.

The Request Queue (Macrotask Queue)

When a roadie finishes the job, the result doesn't go straight to the DJ's table. It goes into the request queue — the Macrotask Queue.

The Event Loop (the DJ's discipline) has one rule: only pick from the queue when the table is completely free.

console.log("1 - DJ opens the set");

setTimeout(() => console.log("3 - First request from the queue"), 0);
setTimeout(() => console.log("4 - Second request from the queue"), 0);

console.log("2 - DJ finishes the opening");

// Output:
// 1 - DJ opens the set
// 2 - DJ finishes the opening
// 3 - First request from the queue
// 4 - Second request from the queue

Even with setTimeout(..., 0), the callbacks go through the queue and only run after the current call stack clears.

Common macrotasks: setTimeout, setInterval, I/O callbacks, setImmediate (Node.js).

The VIP Queue (Microtask Queue)

Here's where it gets interesting.

Some guests at the party are VIPs. They have a separate queue, and the DJ has a strict policy: after each song (macrotask), empty the entire VIP queue before touching the regular queue. In fact, the VIP queue also runs after the initial call stack clears — before the very first song from the regular queue plays.

The Microtask Queue works exactly like this. It's processed completely after every macrotask, before the next macrotask starts.

console.log("1 - Start");

setTimeout(() => console.log("5 - Macrotask (setTimeout)"), 0);

Promise.resolve()
  .then(() => console.log("3 - Microtask 1 (Promise)"))
  .then(() => console.log("4 - Microtask 2 (chained Promise)"));

console.log("2 - End of synchronous code");

// Output:
// 1 - Start
// 2 - End of synchronous code
// 3 - Microtask 1 (Promise)
// 4 - Microtask 2 (chained Promise)
// 5 - Macrotask (setTimeout)

The Promise callbacks (microtasks) always run before the setTimeout callback (macrotask), even though the setTimeout was registered first.

Common microtasks: Promise.then, Promise.catch, Promise.finally, queueMicrotask, async/await continuations.

The Full Set

Let's put it all together with one complete example:

console.log("1 - Sync: party starts");

setTimeout(() => console.log("6 - Macrotask: roadie's job is done"), 0);

Promise.resolve()
  .then(() => {
    console.log("4 - Microtask: VIP guest arrives");
  })
  .then(() => console.log("5 - Microtask: VIP gets their drink"));

setTimeout(() => console.log("7 - Macrotask: second roadie done"), 0);

console.log("2 - Sync: DJ keeps playing");
console.log("3 - Sync: call stack clears");

// Output:
// 1 - Sync: party starts
// 2 - Sync: DJ keeps playing
// 3 - Sync: call stack clears
// 4 - Microtask: VIP guest arrives
// 5 - Microtask: VIP gets their drink
// 6 - Macrotask: roadie's job is done
// 7 - Macrotask: second roadie done

Order of execution:

  1. All synchronous code (call stack drains completely)
  2. All microtasks (VIP queue — fully drained)
  3. One macrotask (regular queue)
  4. All microtasks again (VIP queue — fully drained)
  5. Next macrotask... and so on

3 Golden Rules

  1. One thing at a time. The call stack is single-threaded — no parallelism — it handles many tasks by delegating, not by doing them at the same time.
  2. Microtasks always come before macrotasks. After every macrotask, drain the entire microtask queue first.
  3. Async operations don't block. They're delegated to the environment (roadies), freeing the call stack to keep working.

Understanding these three rules explains 90% of the "weird" behavior you'll encounter with async JavaScript in Node.js.

F
Fernando Callata

© 2026. Excellence in first place.