completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

The Node.js event loop

Learn how Node.js phases, process.nextTick, setImmediate, microtasks, and event loop utilization shape asynchronous work.

By the end, you can
  • 01
    Follow one round of the loopName the five phases, timers, pending callbacks, poll, check, and close callbacks, and say what runs in each.
  • 02
    Pick the right queuePredict when nextTick, promise callbacks, setImmediate, and timers run, and know which orders are always the same.
  • 03
    Measure how busy the loop isRead event loop utilization as working time compared with waiting time, and use it as a reason to look closer, not as an exact timer.

One loop, many kinds of work

Node.js, or Node for short, runs JavaScript outside the browser, for example on a web server. A Node program often waits for slow things: a file to be read, a database to answer, or a message to arrive over the network. Reading and writing like this is called I/O, short for input and output.

Node doesn't stop and wait for I/O. It asks the operating system to do the I/O. It also keeps a callback: a function to run when the I/O is done. In the meantime, Node runs other code.

The Node.js event loop is the part of Node that decides which ready callback runs next. It goes round and round. On each round, it looks for finished work and runs the callbacks that are ready. A library called libuv, which is built into Node, runs this loop.

The event loop doesn't make JavaScript faster. Node runs your JavaScript on one thread. A thread is one line of work that does one thing at a time. So callbacks run one after another, never two at once. If one callback takes a long time, everything else waits: timers, I/O callbacks, and new requests. That is why the order of callbacks matters, and why short callbacks matter.

Three kinds of waiting callbacks

Node keeps waiting callbacks in lists called queues. A queue is a waiting line: the first one in is the first one out. There are three kinds:

  • The phases: the named stops on each round of the loop. Each phase has its own queue.
  • The process.nextTick queue: a Node-only queue that runs between callbacks.
  • The promise queue: callbacks from promises, like the function you pass to .then.

Each kind runs at a different moment. This lesson shows when.

This lesson follows Scheduling work. It is about Node. The event loop, runtimes, and microtasks lessons cover the same ideas in the browser.

The five phases of one round

A phase is one named stop on a round of the event loop. Each phase has its own queue of callbacks. In each phase, Node:

  • does that phase's own work, such as checking which timers are due;
  • runs the callbacks in its queue, oldest first;
  • moves on to the next phase when the queue is empty, or when it has run as many callbacks as it allows at one time.
The five phase names, in orderJavaScript
const phases = ["timers", "pending callbacks", "poll", "check", "close callbacks"];
console.log(phases.join(" -> "));

Line 1 lists the five phases in the order Node visits them. Line 2 joins them with arrows and prints them. Here is what each phase does:

  • timers runs callbacks from setTimeout and setInterval whose time is up.
  • pending callbacks runs a few callbacks for system jobs left over from the last round.
  • poll runs most I/O callbacks, such as "the file has been read". Node can also wait here for new I/O.
  • check runs callbacks from setImmediate.
  • close callbacks runs some "closed" events, such as a network connection that closed suddenly.
One round of the Node event loop. Five cards in order: timers for due timers, pending for left-over jobs, poll for ready I/O, check for setImmediate, and close for close events. An arrow from close goes back to timers for the next round.next roundtimerspendingpollcheckclosedue timersleft-over jobsready I/OsetImmediateclose events
Each round visits the five phases from left to right. After close, the next round starts again at timers.

The table below compares the phases. Read each row as: "this phase does this, and here is one thing to remember about it."

What each phase of the Node event loop does
Which phaseWhat it doesWhat to remember
timersIt runs callbacks from setTimeout and setInterval whose time is up.A delay is a minimum wait, not an exact time.
pending callbacksIt runs a few callbacks for system jobs left over from the last round.Most of your I/O callbacks don't run here.
pollIt runs most I/O callbacks, and it can wait for new I/O.If a setImmediate callback is waiting, Node moves on to check instead of waiting.
checkIt runs setImmediate callbacks, right after poll.It is a different phase from timers.
close callbacksIt runs some close events, such as a connection that closed suddenly.It is not a place for all your clean-up work.
Real-life analogyA waiter's round of the tables

A waiter walks the same round again and again. First they check the kitchen timers. Then they finish jobs left over from the last round. Then they take new orders and bring out food that is ready. Then they do the jobs saved for right after the orders. Last, they clear the tables of guests who left. Then they start again.

In real life: Check the kitchen timers
In JavaScript: timers runs timers whose time is up
In real life: Finish jobs left over from the last round
In JavaScript: pending callbacks runs left-over system jobs
In real life: Take new orders and bring out food that is ready
In JavaScript: poll runs I/O callbacks
In real life: Do the jobs saved for right after the orders
In JavaScript: check runs setImmediate callbacks
In real life: Clear the tables of guests who left
In JavaScript: close callbacks runs close events

Not every round has work in every phase. Node passes quickly through a phase with an empty queue. Also, a timer's delay is a minimum, not an exact time. A 100 ms timer runs at the first timers phase after 100 ms have passed.

Poll makes a choice each time it runs:

  • If an I/O callback is ready, poll runs it.
  • If no I/O is ready but a setImmediate callback is waiting, Node moves on to check.
  • If nothing is ready, poll waits for the operating system to report finished I/O. It stops waiting when a timer is due.

The phases are queues, not five functions inside each other. A callback can add new work while it runs. That new work goes into its own queue. So which callback runs next depends on where the work was added, not only on the list of phases.

A small model of one round

Real Node runs outside the browser, so this page can't show it live. Instead, here is a small model written in plain JavaScript. A model is a simple copy that keeps only the part you want to study. This model:

  • visits the phase names in a fixed order;
  • runs the callbacks you give it, phase by phase;
  • leaves out real timer delays and the operating system.
Step through one round of the model
Step 0 of 6Ready
Your turn: follow the blue line

Step through a small model of one round of the Node event loop. It shows the order of the queues, not what happens inside libuv.

Running in
  1. script
Next: line 1
  { phase: "timers", label: "tea timer" },  { phase: "poll", label: "order ready" },  { phase: "check", label: "after poll" },];const nextTicks = ["read note"];const promises = ["update screen"];console.log(runOneTurn(callbacks, nextTicks, promises).join("\n"));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay calls the lesson's real runOneTurn function. After each phase callback, the model first adds the nextTick labels, then the promise labels. You'll meet these two queues in the next sections. The model shows the order of the queues. It can't show what happens inside libuv.

Playground: add callbacks to one round
A small model of one roundPop out in the code editor (opens in a new tab)JavaScript
const phaseOrder = ["timers", "pending", "poll", "check", "close"]; function runOneTurn(callbacks, nextTicks = [], promises = []) {  const output = [];  for (const phase of phaseOrder) {    for (const callback of callbacks.filter((item) => item.phase === phase)) {      output.push(phase + ": " + callback.label);      output.push(...nextTicks.map((label) => "nextTick: " + label));      output.push(...promises.map((label) => "promise: " + label));    }  }  return output;} console.log(runOneTurn([{ phase: "poll", label: "file ready" }], ["note"], ["then"]).join("\n"));
Order the model prints1 lines
  1. poll: I/O
Try it yourself

The model has 1 callback to run. After each phase callback, it adds every nextTick label, then every promise label.

This model runs in your browser to show the order of the queues. It is not real Node, and it doesn't measure any real timing.

Add one kind of callback at a time. A timer, an I/O callback, or an immediate adds a phase callback. A nextTick or a promise adds a label that the model prints after every phase callback.

Try this: add a timer and an immediate, then add a nextTick. The phase order stays the same, so the new lines are easy to spot. Real Node also has timer delays and operating-system events. That's why this is a model for learning, not code to copy into a real program.

The process.nextTick queue

process.nextTick(callback) is a Node-only function. It puts the callback in the nextTick queue. Node runs this queue as soon as the current code finishes, before the event loop moves on. The name sounds like "on the next round", but it isn't. The nextTick queue is not one of the five phases.

nextTick runs before a promise callbackJavaScript
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
console.log("sync");

Line 1 puts a callback in the nextTick queue. Line 2 makes a promise that is already finished, and its .then puts a callback in the promise queue. Line 3 runs right away. So the output is sync, then nextTick, then promise. The lesson test runs this code in a real Node process to check the order.

This order is for a CommonJS file. That is the older kind of Node file, which loads other files with require.

The printed order. Card 1, sync, from line 3, runs now. Card 2, nextTick, from line 1, runs next. Card 3, promise, from line 2, runs last.ordinary codesyncwaiting in a queuenextTickpromise1. line 3: now2. line 13. line 2
Line 3 prints right away. When the script ends, Node empties the nextTick queue first, then the promise queue.

Use process.nextTick rarely. It is for one case: a function must finish what it is doing before it tells its caller that something happened. Be careful: if a nextTick callback keeps adding another nextTick callback, Node never reaches poll. Then ready I/O waits forever.

A safe example has to stop. Picture a function that reads note 1 and adds note 2 to the nextTick queue. Then it reads note 2 and adds note 3. Node reads all three notes before any setImmediate callback can run. After note 3, nothing new is added, so the loop moves on. A version that never stops would block the loop forever.

Promise callbacks also run before the loop moves on. But in a CommonJS file, Node empties the nextTick queue before the promise queue. An ES module, a file that uses import and export, is different. Its top-level code already runs as a promise job, so there the promise callback can run before the nextTick callback.

For promise code, use promise tools like .then and await. Use process.nextTick only when a function really needs it.

Real-life analogyA note before the next step

Someone hands the waiter a note. The waiter reads it before taking one more step around the room. If people keep handing them new notes, they never get back to the tables.

In real life: The waiter finishes the current sentence
In JavaScript: The current code finishes
In real life: They read the note before taking another step
In JavaScript: Node runs the nextTick queue before the loop moves on
In real life: A pile of notes keeps them from the tables
In JavaScript: Endless nextTick callbacks keep Node from reaching poll

setImmediate versus setTimeout

Both functions run a callback later. They wait in different phases:

  • setImmediate(callback) puts the callback in the check phase, which comes right after poll.
  • setTimeout(callback, delay) puts the callback in the timers phase. It runs once at least delay milliseconds have passed.

The names don't tell you which one runs first. The answer depends on where you call them.

Inside a file read, immediate comes firstJavaScript
import { readFile } from "node:fs";

readFile(process.execPath, () => {
  setTimeout(() => console.log("timeout"), 0);
  setImmediate(() => console.log("immediate"));
});

Line 1 loads readFile, a function that reads a file. Line 3 starts reading a file. Here the file is the Node program itself, process.execPath. When the read is done, the callback on lines 3 to 6 runs in poll. Line 4 adds a timer with a delay of 0. Line 5 adds an immediate. Here, immediate always prints before timeout. The lesson test checks this order in a real Node process.

Inside a file read. Card 1, readFile done, runs in poll and schedules both callbacks. Card 2, immediate, runs next in check and prints first. Card 3, timeout, waits for timers in the next round and prints second.pollreadFile donecheckimmediatenext round: timerstimeoutschedules bothprints 1stprints 2nd
The file-read callback runs in poll. The next phase is check, so immediate prints before timeout, which waits for the next round.

Why? The callback ran in poll. The next phase is check, so the immediate runs there. The timer has to wait for the timers phase of the next round.

In the main file, outside any callback, it's different. If you call both functions at the top level, either one can print first. It changes from run to run, because it depends on how fast Node starts up. So never write a test that expects one order there. If you need work to run after an I/O callback, use setImmediate.

A delay of 0 doesn't mean "now". It means "as soon as possible after a short minimum wait". A busy callback, a long queue, or the operating system can all make the real wait longer.

The table below compares four ways to run code later. Read each row as: "this tool runs at this moment, and this is what it's for."

Four ways to run code later in Node
Which toolWhen it runsWhat to use it for
process.nextTickRight after the current code, before the loop moves on.Rare cases where a function must finish first. Never in an endless chain.
A promise callbackRight after the current callback, after the nextTick queue.The next step of promise code.
setImmediateIn the check phase, right after poll.Work that should run after I/O.
setTimeout(..., 0)In the timers phase, after a short minimum wait.Work for later. Don't use it to force an exact order.

Microtasks between callbacks

A microtask is a small job that runs as soon as the current callback ends. Promise callbacks, like the function you pass to .then, are microtasks. Since Node 11, after each setTimeout or setImmediate callback, Node runs:

  1. the nextTick queue;
  2. then the microtasks;
  3. and only then the next callback in the phase.
Each timer's promise runs before the next timerJavaScript
setTimeout(() => {
  console.log("timeout 1");
  Promise.resolve().then(() => console.log("micro 1"));
}, 0);

setTimeout(() => {
  console.log("timeout 2");
  Promise.resolve().then(() => console.log("micro 2"));
}, 0);

Lines 1 to 4 add the first timer. When it runs, it prints timeout 1 and adds a promise callback. Lines 6 to 9 add a second timer that does the same. The real output is timeout 1, micro 1, timeout 2, micro 2. It is not both timeouts first and then both micros.

The printed order. Card 1, timeout 1, a timer. Card 2, micro 1, its promise callback. Card 3, timeout 2, the second timer. Card 4, micro 2, its promise callback.first timer and its promisetimeout 1micro 1second timer and its promisetimeout 2micro 21. timer2. promise3. timer4. promise
After each timer callback, Node runs the promise callbacks it queued. Only then does the next timer start.
Step through what runs between two timers
Step 0 of 5Ready
Your turn: follow the blue line

Replay a small model of what runs between two timer callbacks. The Node-only code above shows the real order.

Running in
  1. script
Next: line 1
  { phase: "timers", label: "timeout 1" },  { phase: "timers", label: "timeout 2" },];const promises = ["micro 1"];console.log(runOneTurn(callbacks, [], promises).join("\n"));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay uses the same small model as the playground. It shows the order as labels. The Node-only code above is the real proof of how Node behaves today.

The microtasks run after each callback, not only at the end of a phase. That's why micro 1 fits between the two timers. This matters when a callback adds a small follow-up job: the follow-up finishes before other waiting callbacks start.

Keep the three kinds of callbacks apart:

  • A timer callback is phase work.
  • A promise callback is a microtask, added by your code.
  • A nextTick callback is Node-only, and it runs before the microtasks.

So two pieces of code that look alike can run in different orders.

Event loop utilization

Event loop utilization, or ELU, is a report from Node about how busy the loop was over a stretch of time. It has three numbers:

  • active: the time the loop spent working;
  • idle: the time the loop spent waiting with nothing to do;
  • utilization: active time divided by the total time, a number from 0 to 1.

ELU helps when a server feels slow. But it doesn't time one callback, and it can't tell you which code was slow.

Print the names in an ELU reportJavaScript
import { performance } from "node:perf_hooks";

const start = performance.eventLoopUtilization();
setImmediate(() => {
  const report = performance.eventLoopUtilization(start);
  console.log(Object.keys(report).sort().join(","));
});

Line 1 loads performance, Node's tool for measuring time. Line 3 takes a first reading and calls it start. Line 4 adds a callback to the check phase, so the next lines run a little later. Line 5 asks for the change since start. Line 6 prints the report's names: active,idle,utilization. The test checks only these names, because the numbers change from run to run.

A high utilization means the loop was working, not waiting, for most of that time. To find out why, look further. Use a CPU profile, a record of which functions used the most time. Count the requests, and time the slow callbacks. Then decide whether the cause is your JavaScript, a file read that blocks, or a busy service.

Here is a simple example. Take 10 units of time. The loop is active for 8 units and idle for 2. Then the utilization is 8 / (8 + 2), which is 0.8. The real report has more detail, but this sum shows why a value close to 1 needs a closer look.

Compare like with like: the same route, similar traffic, and a similar stretch of time. A quiet server can have low utilization while one request is slow for another reason. A busy server can have high utilization while doing useful work. So look at ELU together with response times and a CPU profile.

Measure a stretch of time, not a moment

ELU is most useful when you compare similar stretches of time. Look at it together with response times and CPU data. One high number tells you to look closer. It doesn't tell you what is wrong.

Practical Node code

Most Node programs never move callbacks between phases by hand. You keep the loop healthy in three ways:

  • Keep request callbacks short.
  • Avoid blocking work in code that runs on every request. Blocking work stops everything until it is done, like readFileSync.
  • Send heavy CPU work, like big calculations, to a worker thread or another service.

Pick the tool that fits the job:

  • Use setImmediate for work that should run after I/O.
  • Use promises for promise code.
  • Use process.nextTick only in rare cases, because it runs before the loop can move on.

For example, a request handler can start a database read and hand control back to Node. When the result is ready, keep that callback small: check the data and send the answer. Don't parse a huge report, resize many images, or run a long regular expression in that callback. Those jobs still use the one JavaScript thread.

If the CPU work can't be made smaller, move it to a worker thread, a job queue, or another service. The event loop is then free to handle I/O and new requests. One slow job no longer holds up the whole server.

Phase, queue, or measurement?
  • poll
  • check
  • process.nextTick
  • A promise .then callback
  • ELU active
  • ELU utilization
Try it yourself
0 of 6 correct

Sort the six cards. Is each one a phase, a queue that runs between callbacks, or an ELU number?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

The next lesson, libuv & the thread pool, goes deeper. It explains which I/O the operating system handles, and which work libuv sends to its own helper threads.

Common misconceptions

  • "Node runs JavaScript in parallel." Your callbacks run one at a time on one thread, even while the operating system does I/O somewhere else.
  • "A delay of 0 means now." A 0 ms timer still waits a short minimum time, and other work can make it wait longer.
  • "nextTick is the next phase of the loop." It is a separate Node-only queue. Node runs it before the loop moves on.
  • "setImmediate always runs first." That is only certain inside an I/O callback. In the main file, the order can change from run to run.
  • "A high ELU proves there is a bug." It shows the loop was busy for that stretch of time. It doesn't show which code made it busy.

When an order puzzles you, first ask where the callback was added: in the main file, in an I/O callback, in a timer callback, or in a promise callback. Then ask which queue it went into. This works better than guessing from the function's name.

In tests, only check orders that are always the same. These two are: nextTick before a promise callback in a CommonJS file, and an immediate before a 0 ms timer inside an I/O callback. In the main file, the order of setImmediate and setTimeout is never certain, so don't test it.

Practice exercises

Exercise 1 · Warm-upPredict the order

Read this Node-only program. Type the order it prints, with commas between the words.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const output = [];
process.nextTick(() => output.push("nextTick"));
Promise.resolve().then(() => output.push("promise"));
output.push("sync");
setImmediate(() => console.log(output.join(",")));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the phase

    Which phase runs a callback from setImmediate?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFirst inside a file read

      In the readFile example, which label prints first?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind what runs in between

        In the two-timer example, what is printed between the first and the second timeout line?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeExplain a busy loop

          In plain words, what does a high ELU utilization value mean?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const report = { idle: 2, active: 8, utilization: 0.8 };
          console.log(report.utilization > 0.7 ? "busy" : "idle");

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a web app

            Your server compresses a huge report on every request, and requests start to wait in line. What should it do with the CPU-heavy job?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, first ask where the callback was added. Then name its queue or phase. This works better than treating every "run later" function the same way.

              Node event loop quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which phase runs setImmediate callbacks?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this Node-only program print?

                Read the code, then predictJavaScript
                process.nextTick(() => console.log("nextTick"));
                Promise.resolve().then(() => console.log("promise"));
                console.log("sync");

                Choose an answer to see the explanation.

              3. Question 3 of 7Where does process.nextTick fit among the five phases?

                Choose an answer to see the explanation.

              4. Question 4 of 7An fs.readFile callback adds both a setImmediate and a setTimeout(..., 0) callback. Which runs first?

                Choose an answer to see the explanation.

              5. Question 5 of 7Since Node 11, what happens after each timer or setImmediate callback?

                Choose an answer to see the explanation.

              6. Question 6 of 7What can performance.eventLoopUtilization() report?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why is it risky when a nextTick callback keeps adding another process.nextTick callback?

                Choose an answer to see the explanation.

              Key takeaways

              • The five phases of a round are timers, pending callbacks, poll, check, and close callbacks.
              • process.nextTick is not a phase. Its queue runs before the loop moves on.
              • Inside an I/O callback, setImmediate runs before a 0 ms setTimeout. In the main file, the order can change.
              • Today's Node runs the nextTick queue and promise microtasks after every timer or immediate callback.
              • ELU reports idle, active, and utilization for a stretch of time.

              Remember the one-liner.
              The Node event loop goes round its phases and hands ready work to your JavaScript, one callback at a time.

              When an order puzzles you, ask three questions. Where was the callback added? Which queue holds it? What must finish first? The answers explain the order, and you can check them with a test.

              Coming next: libuv & the thread pool. It shows which I/O the operating system does, and which work goes to libuv's helper threads.

              CompleteFrontend Clear concepts. Working examples.