Skip to content

process.nextTick vs promise microtasks, and starvation

nextTick is not the next loop turn: Node empties the tick queue, then the promise queue, again and again, before the loop moves on.

Predict first

Before you read: predict

ExamHall's proctor console server receives candidate events on a socket. Its 'data' handler is below.

In what order do the five log lines print?

Code for this question
socket.on('data', (evt) => {  setImmediate(() => log('forward'));  Promise.resolve().then(() => {    log('dedupe');    process.nextTick(() => log('audit'));  });  process.nextTick(() => log('parse'));  queueMicrotask(() => log('count'));});
Pick one

The 30-second version

process.nextTick has its own queue, separate from promise microtasks. When a callback returns, Node runs every queued nextTick, including new ones, then every microtask, including new ones, and repeats until both are empty. Ticks added while microtasks run come after all of them. Top-level ES module code already runs inside a microtask pass.

nextTick is not the next loop turn: Node empties the tick queue, then the promise queue, again and again, before the loop moves on.

Step through the code

Trace of the opening question pattern

Play options

Ready: press Play or step with Next frame

Row 1 of 4 · After the handler returns

Starting row · computed by comparing each row with the one before

  • After: the handler returns
  • Tick queue: one tick
  • Microtask queue: two callbacks
  • Runs: the tick

Rule · step 2

After each callback (the main script, a timer, an I/O or setImmediate callback), Node runs every queued nextTick, including ticks added meanwhile.

After
the handler returns
Tick queue
one tick
Microtask queue
two callbacks
Runs
the tick
Step
2
Trace of the opening question pattern
AfterTick queueMicrotask queueRunsStep
the handler returnsone ticktwo callbacksthe tick2
the tick queue is empty (changed)empty (changed)two callbacksboth, in FIFO order; the first queues a tick (changed)3, 4
the microtask queue is empty (changed)one tick (changed)empty (changed)the new tick (changed)4, 2
both are empty (changed)empty (changed)emptythe loop moves on (where the immediate runs: node-event-loop-phases) (changed)6

Space plays or pauses · ←/→ one row · Home/End first/last

Rows come from the pack's own trace table: a generic handler; TRUTH rows of model.mjs.

The probe code (rows are not linked to code lines)
The probe code
socket.on('data', (evt) => {  setImmediate(() => log('forward'));  Promise.resolve().then(() => {    log('dedupe');    process.nextTick(() => log('audit'));  });  process.nextTick(() => log('parse'));  queueMicrotask(() => log('count'));});

How it works

Step 1 of 12

Two queues. Node keeps a nextTick queue. V8 keeps the microtask queue: promise reactions, await continuations and queueMicrotask, as the prerequisite describes.
See it run

Not in this run

Analogy: A town council meeting

The chair runs the meeting by one rule about two lists.

In the council chamber In Node
when a speaker sits down, the chair first hears every point of order, including ones raised meanwhile the tick queue drains after each callback (step 2)
then every follow-up question on the list, in sign-up order the microtask pass, FIFO (step 3)
a point of order raised during questions is heard once the question list is empty a tick queued in a microtask waits (step 4)
the next agenda item and the public gallery wait until both lists are empty the loop continues only then (step 6)
a member who raises a new point of order in every point of order stalls the meeting recursive nextTick starves everything (step 7)
the chair's opening address is given during question time ES-module top level (step 10)

In real terms: after every callback Node empties two queues in turn, ticks first, and the loop waits for both.

Breaks down when: a real chair can rule a point of order out of order. Node has no cap: the docs warn that recursive nextTick can create an infinite loop.

Contrasting case: the browser has no tick queue. There, promise callbacks and queueMicrotask share the only microtask queue, the prerequisite's model.

Examples, run for real

4.1 ExamHall: the proctors' dashboard froze

EXH-1330: the proctor console server dedupes a backlog of candidate events in batches, inside a socket callback. Each batch re-scheduled the next with process.nextTick. A 50 ms timer pushes the proctors' live dashboard; while the backlog lasted, it never fired, and the dashboard froze. MR !1530 re-schedules with setImmediate.

const step = () => { dedupeBatch(); if (more()) reschedule(step); };   // 100 batches, 10 ms of CPU each
setInterval(pushDashboard, 50);                                        // the proctors' live view
4.1 process.nextTick (as shipped)   batches 100/100 · dashboard pushes during dedupe: 0
4.1 setImmediate (MR !1530)         batches 100/100 · dashboard pushes during dedupe: at least 1

With nextTick the tick queue never empties, so the loop never reaches its timers (step 7). Why setImmediate lets the loop move on is node-event-loop-phases step 7. The work and its total CPU are unchanged; only who waits changes.

4.2 Where the entry module type matters (fixtures/entry.cjs, fixtures/entry.mjs)

The same three lines, then, nextTick, queueMicrotask, run at the top level and again inside a setImmediate callback, in a CommonJS file and in an ES module:

4.2 CommonJS  top level                    nextTick · then · microtask
4.2 CommonJS  inside a setImmediate        nextTick · then · microtask
4.2 ES module top level                    then · microtask · nextTick
4.2 ES module inside a setImmediate        nextTick · then · microtask

Only the ES module's top level differs: it is already inside a microtask pass, so the tick waits (steps 4, 10). Inside a callback both agree (step 11). A test that pins an order at the top level of an .mjs file can pass while the same code, run from a request handler, behaves differently.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 4

A chat server's 'data' handler first calls readState.then(markRead), where readState is an already-resolved promise. Then it starts a fan-out: fanOut(i) sends the message to member i, then calls process.nextTick(() => fanOut(i + 1)), until all 2,000 members have it.

After how many sends does markRead run?

Pick one

Question 2 of 4

A payment SDK's Gateway constructor calls process.nextTick(() => this.emit('ready')). A CommonJS checkout service has async function openCheckout() { const gw = new Gateway(); const rates = await cachedRates; gw.on('ready', startTaking); }, where cachedRates is an already-resolved promise and startTaking needs rates, so the listener must stay after the await. Its unit test, an ES module, calls await openCheckout() at its top level, and startTaking runs. In production, openCheckout() runs inside a request's 'data' callback, and startTaking never runs.

Which one change makes production work and keeps the test passing?

Pick one

Question 3 of 4

A box office's 'data' handler starts a seat sweeper with queueMicrotask(sweep); sweep frees one expired hold and re-queues itself with queueMicrotask until 3,000 are freed. Next, the handler calls payment.then(() => sdk.confirm()) on an already-settled payment, and confirm emits 'settled' from process.nextTick. 'settled' fires only after all 3,000 holds are freed.

Which is the bug?

Pick one

Question 4 of 4

A streaming relay, a CommonJS app, reads a stream's rows from its database inside a request handler. In the .then callback that receives the rows, it schedules the stream's end with process.nextTick(() => stream.emit('end')), and then calls a helper that defers a cleanup callback. cleanup must run after the current callback returns and before 'end' is emitted.

Which API should the helper use?

Pick one

Go deeper

Pro tips
  1. Pin an ordering test inside a callback, not at the top level of an .mjs test file. Wrap the code under test in setImmediate(…) or a real I/O callback, where production runs it. When: unit tests of event emitters, SDK start-up and anything using nextTick (steps 10–11). Verify (RAN, fixtures/entry.*, 100 runs): the ES module's top level printed then · microtask · nextTick; inside setImmediate, nextTick · then · microtask.
  2. Search hot paths for a nextTick that re-queues itself. It holds back promise callbacks, timers and I/O until the chain ends; re-schedule long work as node-blocking-event-loop describes. When: batch jobs, dedupe, fan-outs in a server (step 7). Verify: with nextTick, the 50 ms dashboard timer fired 0 times during a 100-batch loop.
Source
  • Node.js v24 API, process: process.nextTick(callback[, ...args]) and "When to use queueMicrotask() vs. process.nextTick()": https://nodejs.org/docs/latest-v24.x/api/process.html#processnexttickcallback-args · source doc/api/process.md at tag v24.19.0 (https://raw.githubusercontent.com/nodejs/node/v24.19.0/doc/api/process.md), fetched 2026-09-28. "Stability: 3 - Legacy: Use queueMicrotask() instead", since v22.7.0 and v20.18.0 (step 12). The queue "is fully drained after the current operation on the JavaScript stack runs to completion and before the event loop is allowed to continue. It's possible to create an infinite loop if one were to recursively call process.nextTick()" (steps 2, 7). "every time the 'next tick queue' is drained, the microtask queue is drained immediately after" (step 3). "So in CJS modules process.nextTick() callbacks are always run before queueMicrotask() ones. However since ESM modules are processed already as part of the microtask queue, there queueMicrotask() callbacks are always executed before process.nextTick() ones". "When in doubt, unless the specific capabilities of process.nextTick() are needed, use queueMicrotask()" (step 12). The constructor-emit pattern is the page's MyThing example. Valid as of 2026-09-28.
  • Node.js, The Node.js Event Loop (section process.nextTick()): https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick · fetched 2026-09-28. "the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop" (step 2); "it allows you to 'starve' your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase" (step 7); "process.nextTick() fires immediately on the same phase; setImmediate() fires on the following iteration" (step 6). Valid as of 2026-09-28.
  • Node.js v24 API, globals: queueMicrotask(callback): https://nodejs.org/docs/latest-v24.x/api/globals.html#queuemicrotaskcallback · source at tag v24.19.0, fetched 2026-09-28. "The microtask queue is managed by V8" (step 1). Valid as of 2026-09-28.

Checked against the tools and docs as of 2026-09-28.