Timers & Concurrency Control

Medium100% Free~25 mins#timers#settimeout#setinterval#concurrency#event-loop#microtasks#rate-limiting
Key Learning Objectives
✓

Understand macrotask queuing mechanics for setTimeout and setInterval within the browser and Node.js event loops.

✓

Master the HTML5 4ms minimum timer clamping rule for nested timer invocations.

✓

Understand why microtasks queued inside a macrotask callback execute immediately before the next macrotask runs.

✓

Learn why setInterval causes execution drift and overlapping executions, and how recursive setTimeout eliminates this issue.

✓

Design and implement asynchronous concurrency limiters (task queues/pools) to bound parallel network requests.

The Interview Problem

What is logged to the console when the following timer and microtask scheduling code executes, and how does the event loop interleave microtasks created inside a macrotask callback before processing subsequent macrotasks?

1const order = [];
2
3order.push('sync-1');
4
5setTimeout(() => {
6 order.push('timer-1');
7 Promise.resolve().then(() => order.push('micro-after-t1'));
8}, 0);
9
10setTimeout(() => {
11 order.push('timer-2');
12 console.log(order.join(' '));
13}, 0);
14
15Promise.resolve().then(() => {
16 order.push('micro-1');
17 queueMicrotask(() => order.push('micro-2'));
18});
19
20order.push('sync-2');
Predict Console Output
Interactive Challenge

Select the option that matches what standard ECMAScript prints to the console:

sync-1 sync-2 micro-1 micro-2 timer-1 micro-after-t1 timer-2

sync-1 sync-2 timer-1 timer-2 micro-1 micro-2 micro-after-t1

sync-1 sync-2 micro-1 micro-2 timer-1 timer-2 micro-after-t1

sync-1 micro-1 sync-2 micro-2 timer-1 micro-after-t1 timer-2

V8 Engine Execution Trace
Step 1 of 10 (Line 3)

Pushes 'sync-1' to order synchronously.

Call Stack (Top = Active)
Global Execution Context
Lexical Scope / Bindings
order:['sync-1']
Console Stream
> [empty]

Deep Technical Breakdown

The Event Loop Architecture: Macrotasks vs Microtasks

The JavaScript runtime processes operations through distinct scheduling queues:

  1. Call Stack (Synchronous): Executes frames in LIFO order until empty.
  2. Microtask Queue: Contains Promise reactions (.then, .catch, .finally), queueMicrotask, and MutationObserver callbacks. This queue is exhaustively drained immediately whenever the Call Stack becomes empty, as well as after every single individual macrotask callback.
  3. Macrotask (Task) Queue: Contains timer callbacks (setTimeout, setInterval), DOM event listeners, and I/O callbacks.

Why Microtasks Inside Timers Preempt Subsequent Timers

Prior to Node.js v11, Node processed timers in a batch loop. However, the W3C HTML Living Standard and modern Node.js v11+ mandate that the microtask checkpoint is executed after every single macrotask callback. When timer-1 schedules micro-after-t1, that microtask drains immediately before timer-2 can begin execution.

The 4ms Clamping Rule (HTML Living Standard § 8.5.2)

When nesting setTimeout calls beyond 5 levels (nesting level > 5), browsers enforce a minimum delay of 4 milliseconds, even if delay = 0 was specified. This prevents recursive zero-delay timers from completely monopolizing CPU resources.

setInterval Drift vs Recursive setTimeout

setInterval(fn, 1000) schedules calls at 1000ms intervals regardless of how long fn() takes to execute. If fn() is an asynchronous API call taking 1200ms, calls will stack up or execute with zero gap, causing race conditions and performance degradation. The Senior Pattern: Use recursive setTimeout to guarantee that the next delay interval only begins after the previous operation completes:

javascript
function poll(interval) {
  return setTimeout(async () => {
    try {
      await fetchStatus();
    } finally {
      poll(interval); // interval strictly measured from completion
    }
  }, interval);
}

Asynchronous Concurrency Throttling Pattern

When processing hundreds of concurrent network requests, unleashing Promise.all can overload servers or trigger rate limits. Production systems implement concurrency pools (like p-limit):

javascript
async function asyncPool(limit, tasks) {
  const executing = new Set();
  const results = [];
  for (const task of tasks) {
    const p = Promise.resolve().then(() => task());
    results.push(p);
    executing.add(p);
    const clean = () => executing.delete(p);
    p.then(clean, clean);
    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }
  return Promise.all(results);
}
Common Traps & Mistakes

Assuming `setTimeout(fn, 0)` executes immediately on the next line. It is placed in the macrotask queue and must wait for all synchronous code and microtasks to finish.

Believing all scheduled macrotasks drain before microtasks. Exactly one macrotask executes, followed by a complete drain of all microtasks before the next macrotask begins.

Using `setInterval` for recurring asynchronous network requests without accounting for interval drift and overlapping executions.

Relying on exact millisecond timing with `setTimeout`. Timers represent minimum delay thresholds, not precise execution guarantees.

FAANG Follow-Up Probes
Probe #1

How does requestAnimationFrame fit into the event loop compared to macrotasks and microtasks?

Probe #2

What is timer throttling in background browser tabs, and how do browsers reduce CPU usage for hidden pages?

Probe #3

How does Node.js setImmediate differ from process.nextTick and setTimeout(fn, 0)?