Node.js Timers and Microtasks

Understand Node.js timer thresholds, nextTick and microtask ordering, cooperative yielding, cancellation, and nonoverlapping scheduled work.

Lesson 33 of 43Intermediate10 min read

A timer is a scheduling request, not an appointment that the runtime guarantees to keep at an exact millisecond. JavaScript must finish its current synchronous work before many queued callbacks can run. Node also has nextTick and microtask queues whose behavior differs from timer phases. This guide uses Node 24 and deliberately places its ordering fixture inside a setImmediate callback, avoiding an ambiguous comparison between top-level module evaluation and CommonJS startup. Learning the scheduling boundary is more useful than memorizing one printout and assuming it applies to every program context.

Treat delays as thresholds rather than deadlines

setTimeout asks for a callback to become eligible after a delay. Existing JavaScript work, other callbacks, and runtime scheduling can postpone its execution. A delay of zero does not interrupt the current function, and the runtime applies its own minimum and range rules. Do not implement an exact clock or a business deadline by counting timeout invocations. Store an actual deadline and compare the clock when work runs. For elapsed-time measurement, use an appropriate monotonic clock rather than assuming wall-clock time cannot change.

setInterval schedules repeated work, but an async interval callback can start another operation before the previous operation finishes. That can produce overlapping database writes or downloads. A sequential async loop that awaits work and then awaits a delay gives explicit nonoverlap, although its cadence includes work time. If the product requires fixed schedule times, calculate them and define what to do when work falls behind: skip, merge, or queue missed runs. A timer API cannot decide that policy on the application's behalf.

Understand where microtasks fit

Promise reactions and queueMicrotask callbacks run through the microtask mechanism. process.nextTick has its own Node queue and can run before those callbacks at the boundary shown in the main fixture. The synchronous statements in a callback still run first. A queued callback cannot retroactively execute in the middle of the statement that scheduled it. Several microtasks scheduled in order preserve their relevant queue order, but nested scheduling and module evaluation can change the context around that order.

ES module evaluation itself occurs through asynchronous machinery, so top-level examples can differ from CommonJS examples involving nextTick and promises. This is why the fixture enters a normal setImmediate callback before scheduling its queues. Do not generalize its output into a universal ordering rule for every top-level script. Use queueMicrotask when you need a microtask boundary; use setImmediate when you need to yield toward another event-loop turn. Awaiting an already resolved promise yields through microtasks and may not let pending I/O progress the way a setImmediate boundary does.

Avoid starving I/O with recursive scheduling

An endlessly replenished nextTick queue can delay other event-loop work. Recursive promise or microtask chains can similarly monopolize scheduling boundaries. Breaking a heavy loop into microtasks does not automatically make the server responsive to socket events. For bounded CPU work that can be divided into small pieces, process a limited batch and schedule the next batch with setImmediate. For substantial parallel CPU work, consider worker threads instead of trying to hide the cost in thousands of callbacks.

The practice processes two numbers per batch and deliberately queues a checkpoint before processing begins. The first two values run synchronously, then the function yields. The checkpoint can observe that only two values have been processed before later batches finish. This is an instructional responsiveness check, not a production performance benchmark. The right batch size depends on the actual operation and latency requirements. Also keep total workload bounded: yielding improves opportunities for other work but does not make an unlimited job consume finite resources.

Cancel scheduled work and release process liveness

clearTimeout and clearInterval cancel timer handles. The promise timer APIs accept an AbortSignal so an awaited delay can reject when canceled. Cancellation must also be handled by the code awaiting the delay. A canceled wait does not undo a database update that already completed. Define cancellation boundaries around actual operations and release associated resources in finally. A signal is a cooperative request where the API supports it, not a universal rollback mechanism.

Timer handles normally keep a Node process alive. unref changes that liveness relationship, allowing the process to exit when nothing else requires it. It does not cancel the timer or guarantee that its callback will run before exit. This is useful for optional background housekeeping, but a required cleanup action must not rely on an unreferenced timer. In servers, shut down recurring work explicitly when shutdown starts. Otherwise a forgotten interval can keep the process running after its HTTP listener has closed, or continue issuing operations while shared dependencies are being disconnected.

Node.js documentation and version notes

Node.js Timers and Microtasks Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Ordering nextTick, microtasks and immediates

The complete fixture first waits for a setImmediate callback. Inside that callback it records synchronous start and end markers, then schedules nextTick and two microtask reactions. A later immediate resolves the outer wait after those queue callbacks have run. The final timer uses the promise API with an already aborted signal, so rejection is deterministic and does not depend on waiting for a wall-clock threshold. The output illustrates one explicitly chosen scheduling context. It intentionally does not compare a top-level setTimeout(0) with setImmediate, whose relative order is not a portable exact-output lesson.

JavaScript
import { setTimeout as delay } from 'node:timers/promises';
await new Promise(resolve => {
  setImmediate(() => {
    console.log('callback start');
    process.nextTick(() => console.log('nextTick'));
    queueMicrotask(() => console.log('microtask'));
    Promise.resolve().then(() => console.log('promise'));
    console.log('callback end');
    setImmediate(resolve);
  });
});
const controller = new AbortController();
controller.abort();
try { await delay(1000, undefined, { signal: controller.signal }); }
catch (error) { console.log(error.name); }
Output
callback start
callback end
nextTick
microtask
promise
AbortError

Node.js Timers and Microtasks in Real Projects

Yield between batches with setImmediate

Create a sum function that yields after every two items. Keep the processed count visible to a checkpoint callback, then print the final total. The solution performs its first batch immediately and schedules later batches with setImmediate. The checkpoint was queued first, so it runs before the next batch. Replacing the yield with a long synchronous loop would prevent that intermediate observation, while replacing it with an endlessly replenished microtask chain could delay the checkpoint until completion. The numbers are deliberately tiny: this exercise demonstrates scheduling structure rather than claiming that summing six numbers requires asynchronous machinery.

JavaScript
const values = [1, 2, 3, 4, 5, 6];
let processed = 0;
setImmediate(() => console.log('checkpoint', processed));
function sumInBatches(items) {
  return new Promise(resolve => {
    let index = 0;
    let total = 0;
    function batch() {
      const end = Math.min(index + 2, items.length);
      while (index < end) { total += items[index++]; processed++; }
      if (index === items.length) resolve(total);
      else setImmediate(batch);
    }
    batch();
  });
}
console.log('total', await sumInBatches(values));
Output
checkpoint 2
total 21

Node.js Timers and Microtasks FAQ

Will setTimeout with zero run immediately?

It schedules later eligible work and cannot interrupt the current synchronous function. Actual execution also depends on the event loop and other work. Use a timer for an appropriate delayed scheduling policy, not a precise guarantee of elapsed time. If a business action has a deadline, record that deadline and check it when the action runs. A busy event loop can make any ordinary timer callback late.

Does await Promise.resolve let socket callbacks run?

It creates a microtask continuation, which is different from yielding to another event-loop turn. A repeatedly replenished microtask chain can delay I/O callbacks. When dividing CPU work for responsiveness, use an appropriate turn boundary such as setImmediate and limit each batch. For heavy parallel computation, worker threads are more suitable. Await syntax alone does not establish that the underlying operation is nonblocking or fair to I/O.

Why are some top-level nextTick examples different?

CommonJS startup and ES module evaluation use different surrounding scheduling contexts. ES module execution can already be inside microtask processing. The guide enters a setImmediate callback to make its chosen boundary explicit. Compare examples only after identifying module format and callback context. A printed ordering without that context is not a reliable specification for unrelated programs or a reason to depend on a fragile race.

Does unref cancel a timeout?

No. It changes whether that timer keeps the process alive. The callback can still run if other active work keeps Node running, but the process may exit first when no referenced work remains. Use clearTimeout for cancellation, or an AbortSignal with a compatible promise timer. Required shutdown actions should run in an explicit cleanup sequence, not depend on an optional unreferenced callback that might never execute.

Node.js Timers and Microtasks Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

How many items have been processed when the worked checkpoint prints?

Node.js Timers and Microtasks Quiz

2 questions to check what you learned.

  1. What does a timeout delay promise?

    • An eligibility threshold, not an exact execution time
    • An interruption of any currently running JavaScript
    • A guaranteed callback time unaffected by CPU work
  2. How can an async setInterval callback overlap its own work?

    • A later tick can begin while an earlier awaited operation is unfinished
    • await automatically stops future interval ticks
    • Intervals are limited to one invocation for the process lifetime