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.
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.
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.
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.
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.
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.
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.
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);
}
AbortError
The timer is cancelled through the same signal that a larger operation can share.
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.
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.
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.
Explore 500+ free tutorials across 20+ languages and frameworks.