NNODE LOOP LABruntime observatoryNNEON · Articles in 90 languages
CONNECTING
v24.18.0linux/x64
One stack, many ready tasks
03

Callback queue

Five zero-delay timers show how one expensive callback delays the entire queue.

PROCESS IDcurrent server
UPTIMEsince startup
LOOP DELAY P95perf_hooks
UTILIZATIONevent loop
HTTP ROUNDTRIPbrowser → server
LIVE TRACE

Timeline

READY
0 ms
Events will appear hereRun the selected scenario
#TIMESOURCEEVENT
Waiting for an experiment…
CHAPTER 03
DEEP DIVE · FROM BASICS TO CODE

Understanding: Callback queue

You can read this chapter before running the experiment. Then return to the live trace and match each concept to a real event.

FIRST, IN PLAIN LANGUAGE

Imagine one service window. Five people may already be ready, but the clerk handles only one person at a time. If the first customer takes a long time, everyone else waits.

TECHNICAL FOUNDATION

A queue stores ready work; it does not execute it. The JavaScript call stack remains the executor. After every callback, priority nextTick and microtask queues may run before the next timer callback. This lab measures from registration, so its number is broader than pure queue waiting time.

Why it mattersQueue delay affects the latency of the entire server. One slow callback can delay thousands of otherwise independent connections.
WHERE THE WORK RUNS
01YOUR JS CODEfunctions · callbacks
02NODE APIsfs · crypto · timers
03V8 + LIBUVheap · loop · pool
04OPERATING SYSTEMI/O · threads · memory
01 · GLOSSARY

Terms used in this experiment

Understand the words first, then the execution order.

01

Queue

A waiting structure. Being queued does not mean the callback is currently executing.

02

Lag

Delay relative to an expected moment. Here the value is total time since registration, including the minimum timer threshold and stack waiting.

03

Run-to-completion

The current callback runs until it returns; the Event Loop does not preempt it.

04

Starvation

A priority queue keeps refilling and prevents other phases from receiving execution time.

02 · MECHANICS

What happens step by step

Each step maps to an observable runtime state.

  1. 01
    Five registrations

    All setTimeout(0) calls are created in one short synchronous loop.

  2. 02
    Timers become ready

    Their minimum deadline passes, but the stack may still be busy.

  3. 03
    First callback blocks

    A CPU loop owns the only main JavaScript stack for about 260 ms.

  4. 04
    Priority queues run

    nextTick and Promise from the first callback run before timer #2.

  5. 05
    The queue drains

    The remaining callbacks execute quickly, one after another.

03 · CONTEXT

Where the result needs context

These details explain why similar code can sometimes produce a different trace.

01

setTimeout(0) is not ready instantly

Zero defines a minimum threshold, not an immediate call. The timer must become eligible, the Event Loop must reach timers, and the stack must be free.

02

The trace is not pure queue wait

The experiment counts milliseconds from common registration. That is useful end-to-end latency, but exact queue wait would require a separate timestamp for each timer’s internal readiness.

03

FIFO here is controlled, not global

Five identical timers are created by one loop and normally run in registration order. Do not extend that result to callbacks from different phases, I/O sources, or threads.

04

Microtasks can cause starvation

After callback #1, Node runs its nextTick and Promise before timer #2. If nextTick recursively keeps adding itself, other Event Loop phases can be starved.

01
Theory

First understand which parts of Node participate in execution.

02
Simplified code

Then remove instrumentation and focus on the central mechanism.

03
Runtime code

Finally match the model to the code that produces the live trace.

04 · Simplified code

A minimal model without instrumentation

src/demos.js · educational snippetJavaScript
for (let i = 1; i <= 5; i++) {
  setTimeout(() => {
    if (i === 1) heavyCpuWork(260);
    console.log('timer', i);
  }, 0);
}
05 · Runtime code

The complete code executed by the scenario

This is not an alternative example: these are the functions and files used by the Run button.

ACTUAL SOURCE

The source is generated from the real server function. Scenarios using a child process or Worker include every participating file.

src/demos.js
scenario74 lines
import { performance } from 'node:perf_hooks';

function blockMainThread(durationMs) {
  const startedAt = performance.now();
  let iterations = 0;

  // Намеренная блокировка для учебного сценария. В production так делать нельзя.
  while (performance.now() - startedAt < durationMs) {
    iterations += Math.sqrt((iterations % 10_000) + 1);
  }

  return Math.round(iterations);
}

async function callbackQueue(emit) {
  const scheduledAt = performance.now();
  emit('call-stack', 'sync', 'Одновременно ставим пять setTimeout(0)');

  await new Promise((resolve) => {
    let completed = 0;

    for (let index = 1; index <= 5; index += 1) {
      emit('timers', 'schedule', `Таймер #${index} зарегистрирован`);

      setTimeout(() => {
        const lag = Math.round(performance.now() - scheduledAt);
        emit(
          'timers-queue',
          'callback',
          `Callback #${index} взят из очереди (через ${lag} мс)`,
        );

        if (index === 1) {
          emit(
            'call-stack',
            'warning',
            'Callback #1 занимает главный поток на 260 мс',
          );
          blockMainThread(260);
          emit('call-stack', 'sync', 'Callback #1 освободил стек');

          process.nextTick(() => {
            emit(
              'nextTick',
              'callback',
              'nextTick из callback #1 вклинился перед следующим таймером',
            );
          });

          Promise.resolve().then(() => {
            emit(
              'microtasks',
              'callback',
              'Promise из callback #1 тоже выполнен перед следующим таймером',
            );
          });
        }

        completed += 1;
        if (completed === 5) {
          // setImmediate даёт nextTick/Promise последнего callback'а выполниться
          // до завершения учебного сценария.
          setImmediate(resolve);
        }
      }, 0);
    }
  });

  emit(
    'result',
    'result',
    'Очередь не исполняет callbacks параллельно: каждый ждёт свободный стек',
  );
}

The emit(...) calls become live-trace rows. await and Promise keep the HTTP stream open until the scenario completes.

06 · DO NOT CONFUSE

Common misconceptions

The myth is on the left; the accurate model is on the right.

MYTH

Ready callbacks execute simultaneously.

ACTUALLY

Readiness grants the right to wait, not parallel JavaScript execution.

MYTH

All queues are globally strict FIFO.

ACTUALLY

FIFO may apply inside a structure, while work categories follow different priority rules.

MYTH

Timer delay means the clock is inaccurate.

ACTUALLY

It usually indicates a busy stack, a saturated phase, or process load.

MYTH

Five expired timers mean five parallel executors.

ACTUALLY

They only become candidates for execution; one main JavaScript stack still processes callbacks sequentially.

07 · SELF-CHECK

Explain it in your own words

If you can explain the answer without quoting documentation, your mental model is starting to take shape.

  1. Why does callback #2 receive roughly 260 ms of lag?
  2. Can a Promise inside callback #1 run before callback #2?