cf.completefrontendCode editorOpen lab
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
    Trace Node phasesExplain timers, pending callbacks, poll, check, and close callbacks without treating them as JavaScript call-stack frames.
  • 02
    Choose a queuePredict when nextTick, promise reactions, setImmediate, and timers are appropriate and where their order is stable.
  • 03
    Measure pressureInterpret event loop utilization as active versus idle time and use it as a signal, not a timing promise.

One loop, many kinds of work

The Node.js event loop is libuv's repeated process for deciding which ready callbacks Node can run next. It lets one JavaScript thread start non-blocking I/O, return to other work, and later run the callback when the operating system reports completion.

The loop is not a magic speed-up. JavaScript callbacks still run one at a time on the main thread. A long callback blocks later timers, I/O callbacks, and requests from making progress, so queue order and small callbacks matter.

A useful map

Node divides its loop into named phases. Around callback boundaries it also checks the Node-specific process.nextTick queue and JavaScript promise microtasks. Those queues have different jobs from the phases.

This lesson follows Scheduling work. It focuses on Node, while the published event loop, runtimes, and microtasks lessons give the shared browser-side foundation.

The libuv phases

A phase is a named part of one event-loop round with its own FIFO callback queue. Node does phase-specific work, runs callbacks in that queue, then moves on when the queue is empty or a system limit is reached.

The five application-facing phase namesJavaScript
const phases = ["timers", "pending callbacks", "poll", "check", "close callbacks"];
console.log(phases.join(" -> "));

Line 1 names the phases this lesson uses. Timers handles due timer callbacks. Pending callbacks handles selected deferred system callbacks. Poll receives most I/O callbacks and can wait for I/O. Check runs setImmediate, and close callbacks handles selected close events.

The phases you can reason about in application code
PhaseWhat it doesKeep in mind
timersRuns callbacks for due setTimeout and setInterval timers.A delay is a threshold, not an exact appointment.
pending callbacksRuns some callbacks deferred from earlier system operations.Most application I/O callbacks do not live here.
pollReceives and runs many I/O callbacks and may wait for I/O.The loop can move to check when an immediate is waiting.
checkRuns setImmediate callbacks after poll completes.It is a separate phase from timers.
close callbacksRuns some close events, such as abrupt socket closes.It is not a general cleanup queue.
Real-life analogyA waiter's repeated round

A waiter walks the same round again and again. They check alarms, finish left-over jobs, take new orders and bring ready food, do right-after-round jobs, then clear tables from guests who left.

In real life: Check the kitchen alarms
In JavaScript: timers checks due timer callbacks
In real life: Finish jobs left from last round
In JavaScript: pending callbacks run
In real life: Take orders and bring ready food
In JavaScript: poll handles I/O
In real life: Do right-after-round jobs
In JavaScript: check runs immediates

Where the analogy stops: A real event loop has platform details and limits; the waiter story only gives the broad round order.

Do not read this as a promise that every round has work in every phase. Poll can wait when nothing is ready, and due timers are thresholds rather than exact scheduled moments.

Poll has an important decision to make. If an I/O callback is already ready, it runs that callback. If no I/O is ready but an immediate is waiting, Node can move to check. If neither is ready, poll may wait for the operating system to report I/O or until a timer threshold matters.

The labels describe queues, not five nested JavaScript functions. A callback can schedule more work while it runs. That new work joins its own queue, so the next visible callback depends on context as well as the broad phase map.

A modeled loop turn

Before reading real Node-only output, use a small model. It has plain phase names and walks one list in a fixed order. It deliberately leaves out platform internals, timer thresholds, and operating-system behavior.

Step through one modeled loop turn
Step 0 of 6Ready
Your turn: follow the blue line

Step through a small, instrumented model of a Node event-loop turn. It teaches ordering without claiming to reveal libuv internals.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  { 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"));
CallStoreChangeResultRun = next line. 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 modeled callback it drains the supplied nextTick labels first and promise labels second. This gives you a stable picture of the priorities without pretending a browser can expose libuv.

Playground: add callbacks to one modeled turn
A small phase modelPop 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"));
Modeled run order1 lines
  1. poll: I/O
Try it yourself

The model has 1 scheduled item. It reads nextTick and promise entries after each phase callback.

This browser-safe model teaches queue relationships. It is not a copy of libuv and does not measure your browser or Node runtime.

Add one kind of callback at a time. A timer, I/O callback, or immediate creates a phase callback. A nextTick note and a promise reaction appear after each phase callback in this teaching model.

Try adding a timer and an immediate, then add a nextTick note. The model keeps the phase callback order fixed so the new line is easy to see. Real Node also has timer thresholds and operating-system events, which is why this is a learning model rather than a scheduler you should copy.

The process.nextTick queue

The nextTick queue is a Node queue that runs after the current operation finishes and before the event loop continues. It is not one of the five displayed libuv phases, despite the name making it sound like a later loop turn.

Synchronous code, nextTick, then promiseJavaScript
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
console.log("sync");

Line 1 queues the Node callback. Line 2 queues a promise reaction. Line 3 runs now, so the real Node output is sync, then nextTick, then promise. The lesson test proves this order in a child Node process.

Use process.nextTick sparingly when an API needs to finish the current stack before notifying its caller. A recursive chain can keep Node from reaching poll, which delays ready I/O. It is a priority note, not a general-purpose work scheduler.

A safe demonstration must stop. Imagine a function that reads note 1, queues note 2, then reads note 2 and queues note 3. Node reads all three notes before an immediate can run; after the third note, the loop can continue. An unbounded version never reaches that final step.

Promises are also urgent compared with later phase work, but Node drains process.nextTick before ordinary promise reactions at this boundary. Prefer promise APIs when you are continuing a promise chain. Use nextTick only when an API specifically needs that stronger ordering.

Real-life analogyA note before the next step

Imagine the waiter has a note in hand. They read it before taking even one more step around the room. If someone keeps handing them notes, they never return to the tables.

In real life: The waiter finishes the current sentence
In JavaScript: The current operation completes
In real life: They read the note before walking
In JavaScript: nextTick drains before the loop continues
In real life: A pile of notes delays the round
In JavaScript: Recursive nextTick can starve poll

Where the analogy stops: The queue has precise runtime rules; this analogy does not model every callback or microtask checkpoint.

setImmediate versus setTimeout

setImmediate schedules a callback for the check phase after poll. setTimeout schedules a callback after at least its delay threshold. Their names do not describe a universal race that application code should depend on.

Inside I/O, immediate is firstJavaScript
import { readFile } from "node:fs";

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

Line 3 begins an I/O callback. Lines 4 and 5 queue a zero-delay timer and an immediate from inside that callback. In this I/O case, immediate always prints before timeout; the child-process test proves that stable order.

In the main module, calling both functions together can produce either order between runs. Do not assert either main-module order in a test. If you need work after an I/O callback, use setImmediate for the documented check-phase relationship.

The small difference in context is the whole lesson. At top level, startup timing can decide whether a zero-delay timer is ready when Node chooses work. Inside the file-read callback, Node has just finished poll work and can enter check, so the immediate has the stable first position.

“Zero delay” means eligible after a minimum threshold, not “interrupt everything now.” Keep that sentence nearby when reading logs. A busy callback, a full queue, or operating-system scheduling can all make the observed delay longer.

Similar APIs, different queues
APIWhen it runsUse it for
process.nextTickAfter the current operation, before the loop continues.Small API deferrals; never unbounded recursion.
Promise reactionAt the microtask checkpoint after the current callback.Continue promise-based work.
setImmediateThe check phase, after poll.Run work after I/O when that relationship matters.
setTimeout(..., 0)Timers after its minimum delay threshold.Schedule later work; do not use it for exact order.

Microtasks between callbacks

A microtask is promise continuation work that Node checks after a callback. Since Node 11, Node runs the nextTick queue and microtasks after each setTimeout or setImmediate callback, before it starts the next callback in that queue.

Two timers with a promise reactionJavaScript
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 through 4 schedule the first timer and its promise reaction. Lines 6 through 9 schedule the second timer. The real output is timeout 1, micro 1, timeout 2, micro 2, not two timeouts followed by two microtasks.

Step through the checkpoint between callbacks
Step 0 of 5Ready
Your turn: follow the blue line

Replay a teaching model of the checkpoint between callbacks. The separate Node-only snippet provides the real runtime evidence.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  { phase: "timers", label: "timeout 1" },  { phase: "timers", label: "timeout 2" },];const promises = ["micro 1"];console.log(runOneTurn(callbacks, [], promises).join("\n"));
CallStoreChangeResultRun = next line. 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. Its labels make the checkpoint visible; the Node-only snippet above is the real runtime evidence for current Node behavior.

The checkpoint happens after a callback, not only after an entire phase queue empties. That is why the first promise reaction fits between the two timer callbacks. This matters when one callback schedules a small follow-up that must settle before unrelated queued callbacks begin.

Keep the queues separate in your head. A timer callback is phase work. A promise reaction is microtask work created by code. A nextTick callback has Node-specific priority. Similar-looking asynchronous code can therefore have different ordering rules.

Event loop utilization

Event loop utilization, or ELU, is a Node measurement of how much sampled time the loop was active instead of idle. It is useful when a server feels busy, but it is not a stopwatch for one callback and it cannot tell you why work was expensive.

Read ELU keys after one immediateJavaScript
import { performance } from "node:perf_hooks";

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

Line 3 takes a starting snapshot. The immediate creates a later observation point. Line 5 asks for the difference, and line 6 prints the stable keys active,idle,utilization. The test checks keys, never exact values or timings.

A high utilization value means the loop spent a high share of that interval active rather than idle. Investigate with CPU profiles, request rates, and slow callback measurements before deciding whether the cause is JavaScript, synchronous I/O, or a busy service.

Think of a ten-unit sample window. If the loop is active for eight units and idle for two, the simple ratio is 8 / (8 + 2), or 0.8. The runtime report has more detail than this teaching calculation, but the ratio explains why a value near one deserves attention.

Compare like with like: the same route, similar traffic, and a meaningful time window. 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 pair ELU with latency and a CPU profile.

Measure a window, not a moment

ELU is most useful as a trend across comparable periods. Pair it with latency and CPU evidence; a single high number is a signal to investigate, not a diagnosis.

Practical Node code

Most Node applications do not manually move callbacks between phases. You make the loop healthier by keeping request callbacks short, avoiding synchronous work in a hot server path, and using a worker or another service for CPU-heavy jobs.

Use setImmediate when work should follow I/O. Use promise APIs for promise-shaped async code. Reserve process.nextTick for narrow compatibility or API deferral cases, because it has stronger priority than ordinary loop progress.

For example, a request handler can start a database read and return control to Node. When the result is ready, keep its callback focused on validating data and responding. Do not parse a huge report, resize many images, or run a long regular expression in that callback; those jobs still occupy the one JavaScript thread.

When CPU work cannot become smaller, move it to a worker thread, a job queue, or another service. That does not make every order concern disappear, but it lets the event loop return to I/O and new requests instead of holding the server hostage to one expensive job.

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

Sort the six cards by what each name describes in this lesson.

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

For deeper Node runtime design, the next lesson explains libuv & the thread pool. It separates kernel-backed I/O from work that libuv sends to its worker pool.

Common misconceptions

  • “Node runs JavaScript in parallel.” Main-thread callbacks run one at a time, even while the system handles I/O elsewhere.
  • “Zero means now.” A zero-delay timer is eligible after a threshold; other work can delay it.
  • “nextTick is the next event-loop phase.” It is a separate high-priority queue checked before the loop continues.
  • “Immediate always wins.” Only the I/O-callback case has the stable relationship taught here; main-module order varies.
  • “High ELU proves a bug.” It proves the loop was active in a sampled period, not which code caused it.

When debugging ordering, first identify the context: main module, I/O callback, timer callback, or promise reaction. Then identify the queue. This is more reliable than guessing from API names.

Write only stable orders in tests. The nextTick-before-promise example and the I/O immediate-before-timeout example are stable. A top-level immediate-versus-timeout race is a useful warning, but it is not a product contract.

Practice exercises

Exercise 1 · Warm-upPredict priority

Read the Node-only program. Type the comma-separated order it reports.

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 scheduled by setImmediate?

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

      Exercise 3 · PracticeChoose inside I/O

      Inside the readFile example, which label prints first?

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

        Exercise 4 · PracticeFind the checkpoint

        What output appears between the first and second timeout lines?

        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 during every request and requests queue up. What should it do with the CPU-heavy task?

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

              Check your understanding

              For each question, name the context first, then name the queue or phase. That habit is more dependable than treating asynchronous APIs as a single line.

              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 is process.nextTick in the five-phase diagram?

                Choose an answer to see the explanation.

              4. Question 4 of 7In an fs.readFile callback that schedules both callbacks, which is first?

                Choose an answer to see the explanation.

              5. Question 5 of 7Since Node 11, what happens after each timer or immediate 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 recursive process.nextTick risky?

                Choose an answer to see the explanation.

              Key takeaways

              • Node's application-facing loop phases are timers, pending callbacks, poll, check, and close callbacks.
              • process.nextTick is outside those phases and runs before the loop continues.
              • Inside I/O, setImmediate runs before a zero-delay timer; main-module order can vary.
              • Current Node runs nextTick work and promise microtasks between timer or immediate callbacks.
              • ELU reports idle, active, and utilization for a sampled interval.

              Remember the one-liner.
              The Node event loop is a repeated phase-based handoff from ready work to one JavaScript callback at a time.

              When an ordering puzzle appears, make a small list: where was the callback scheduled, which queue owns it, and what work must finish first? That three-question habit turns an intimidating runtime diagram into a testable explanation. Context decides.

              Coming next: libuv & the thread pool, where Node's kernel-backed I/O and worker-pool work are separated.

              CompleteFrontend Clear concepts. Working examples.