You've got a CPU-heavy job, and the server still has to keep serving requests, so you need to slice it up. setImmediate, setTimeout(0), or queueMicrotask. Pick one and defend it.
Picks the scheduling primitive from queue placement, which queue it lands in and when that drains, and defends the pick with throughput numbers
I'd go with setImmediate for breaking up CPU work. It schedules the next slice after the current event loop phase, so pending I/O gets to run between the slices. setTimeout(0) works too, it's just slower, because Node clamps the delay to a minimum of one millisecond and every slice pays that latency. queueMicrotask is the wrong tool here I think. Microtasks all drain before the loop moves on, so if you queue the next slice as a microtask the loop stays busy and the I/O just keeps waiting. The pattern itself is pretty simple. You process a batch of items, call setImmediate with the next batch, and repeat until the job finishes. The server stays responsive while the work makes progress. For really heavy jobs I'd probably reach for worker threads instead, since they move the work off the main thread entirely.