Tutorials Logic, IN info@tutorialslogic.com

Node.js Event Loop, Microtasks, Timers, and Scheduling

Scheduling Model

The event loop repeatedly runs ready callbacks around asynchronous resources. Correct scheduling depends on phase, context, microtask queues, and whether code keeps the loop occupied or keeps resources referenced.

What the Event Loop Coordinates

Node.js executes ordinary JavaScript on an event-loop thread. When an asynchronous resource becomes ready, its callback is queued for a suitable phase. The loop runs callbacks serially, so one slow callback delays unrelated clients.

The operating system handles much network I/O; a libuv worker pool handles selected operations such as many file-system, DNS, crypto, and compression tasks. Completion still returns to JavaScript through the event loop. The pool is not a general-purpose place for application loops.

  • Keep callbacks bounded.
  • Measure event-loop delay under representative load.
  • Use worker threads for CPU-intensive JavaScript.

Phases and Observable Order

A loop iteration moves through timers, pending callbacks, poll, check, and close-callback work, with internal preparation around them. Poll receives many I/O callbacks; setImmediate callbacks run in check. Timer thresholds mean run no earlier than the delay, not at an exact wall-clock instant.

Do not encode business correctness around a race between setTimeout(fn, 0) and setImmediate(fn) from top-level code. Their observed order can depend on timing and platform context. Inside an I/O callback, setImmediate commonly runs before a zero-delay timer because check follows poll.

Microtasks and process.nextTick

Promise reactions and queueMicrotask callbacks use the microtask queue. Node also has a next-tick queue for process.nextTick callbacks, which is drained before ordinary microtasks at defined boundaries. Both run before the loop advances to another phase.

Recursive nextTick or microtask scheduling can starve timers and I/O because the queues keep refilling. Prefer queueMicrotask for portable microtask semantics and reserve process.nextTick for narrow Node API compatibility needs, such as making a callback consistently asynchronous.

Observe Scheduling Queues

Observe Scheduling Queues
console.log('start');
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
process.nextTick(() => console.log('nextTick'));
console.log('end');

Synchronous output comes first, then nextTick and microtasks. The relative top-level timer and immediate order should not be treated as a contract.

Timers as Resources

setTimeout schedules one callback after a minimum delay; setInterval repeats until cleared; setImmediate schedules check-phase work. Each returns a handle that can be cleared, refreshed, referenced, or unreferenced. An unref timer does not keep the process alive by itself.

Intervals drift when callbacks take time and can overlap downstream operations if each tick starts async work without waiting. For polling, a recursive timeout after completion often gives clearer backpressure and shutdown behavior than setInterval.

Promise-Based Timers and Cancellation

The node:timers/promises module exposes promise versions of timeout, immediate, and interval scheduling. They integrate with await and AbortSignal, making deadlines and shutdown easier to compose than manual callback wrappers.

When a timer owns resources, cancel it during shutdown and remove listeners. A rejected timer promise caused by abort is an expected control-flow outcome when cancellation was requested; classify it separately from a service failure.

Abort a Promise Timer

Abort a Promise Timer
import { setTimeout as delay } from 'node:timers/promises';

const controller = new AbortController();
setTimeout(() => controller.abort(), 25);

try {
  await delay(1000, 'done', { signal: controller.signal });
} catch (error) {
  console.log(error.name);
}
Output
AbortError

The timer is cancelled through the same signal that a larger operation can share.

Blocking, Starvation, and Fairness

Large JSON work, synchronous APIs, expensive regular expressions, compression loops, and unbounded callbacks can block progress. Break optional batch work into bounded chunks, but do not use repeated setImmediate calls as a substitute for moving sustained CPU work to a worker thread.

Backpressure is scheduling discipline for data producers: when a writable stream returns false, wait for drain. Concurrency limits provide the same protection for promise-based producers. Fairness means one request cannot monopolize CPU, memory, or downstream capacity.

Event-Loop Diagnosis

Measure latency distributions and event-loop delay instead of guessing from CPU alone. A process can show moderate CPU while a few long callbacks create severe tail latency. Capture operation names and timings, then reproduce with the same payload size and concurrency.

Test callback order only where Node documents it. Use fake time cautiously because it may not model I/O phases. For shutdown tests, verify no interval, socket, listener, or unfinished promise keeps the process alive unexpectedly.

Before you move on

Event-Loop Reasoning Check

4 checks
  • I can explain why a timer delay is a threshold.
  • I do not depend on a top-level timer versus immediate race.
  • I avoid recursive nextTick and microtask starvation.
  • I measure blocking and know when work belongs in a worker.

Node JS Questions Learners Ask

Application JavaScript normally runs on one event-loop thread, while the operating system, libuv worker pool, and optional worker threads perform other work. The useful answer depends on which work is being discussed.

No. It becomes eligible after the threshold, then waits until the timers phase and until earlier JavaScript work has completed.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.