# The event loop: why a 10 ms timer waits 80 ms

**AI Engineering Visualised** · Coding · Film, 1:29 · Updated 2026-09-12

The event loop is the scheduler that runs JavaScript one task at a time, draining the microtask queue after each task before it looks at timers. A timer's delay is a minimum wait, not a guarantee, because a running task cannot be interrupted.

> Trace an executable TypeScript example through a running task, a nested microtask checkpoint and a waiting timer, then compare promise continuations with real task yields in two browser engines.

Watch the film: https://understandingdata.com/ai-engineering-visualised/event-loop/
Video file: https://assets.understandingdata.com/viz/event-loop/video/film.mp4?v=09937853b8

A timer asks the browser to wait before making a callback eligible. It does not reserve the thread at a deadline. Press play and follow one TypeScript script: it prints start, schedules a 10 ms timer, queues A and B, performs 80 ms of synchronous work, and prints end. The busy helper repeatedly reads performance.now() until the requested interval has passed; it is deliberately blocking work used to expose the scheduling mechanism.

The currently running task keeps the thread until it returns. The film's illustrative clock reaches the timer threshold at 10 ms while busy(80) is still executing. Eligibility changes, but execution does not interrupt the script. In this example the browser can only run the callback after the long task and the following microtask checkpoint have finished.

At that checkpoint, A runs first and queues C. C joins behind the already queued B. The checkpoint keeps taking the oldest microtask until its queue is empty, including microtasks appended by other microtasks. The resulting console order is start, end, A, B, C, timer. The TypeScript fixture executes in Chromium and WebKit, three times each, and the film's finite pure model reproduces that order.

Splitting the same work into four 20 ms chunks is not sufficient by itself. With await Promise.resolve(), each async continuation is a microtask, so the checkpoint keeps running the remaining chunks before the timer gets a turn. With await new Promise<void>((resolve) => setTimeout(resolve, 0)), the current continuation suspends until a later timer task resolves its promise. That resolution queues the async continuation as a microtask. Other tasks now have an opportunity to run between chunks.

The local captures make that opportunity concrete. The observed 10 ms timer ran at about 80 ms after the long task or promise yields. With timer task yields it ran around 40 ms in Chromium and 20 ms in WebKit. Neither value is a browser guarantee. The controls select a stored three-run trace and seek to its film chapter; the engine control changes the measured range and console order beneath the player. These are genuine observations of the same TypeScript source, with capture version and source hashes kept beside the fixture.

For work that affects responsiveness, keep requested delay, eligibility and actual callback execution separate. Yield to another task when appropriate, or move substantial computation to a worker. A promise continuation alone does not give timer tasks a turn. This explainer models one agent, a finite microtask queue and one timer task source. It does not model rendering opportunities, input prioritisation, cross-agent parallelism, background throttling or exact browser timer insertion times. In a real application, profile the work and measure responsiveness under representative load.

## The maths

### Delay and execution are separate

```latex
t_{\mathrm{execute}} \ge t_{\mathrm{eligible}} \ge t_{\mathrm{schedule}} + d
```

The timer delay d is a minimum waiting condition. The film models eligibility at the threshold; the browser can make the callback runnable later and must still obtain a turn to execute it.

### The worked example

```latex
d = 10\,\mathrm{ms},\quad W = 80\,\mathrm{ms},\quad t_{\mathrm{execute}} \approx 80\,\mathrm{ms}
```

The synchronous busy loop holds this agent for 80 ms. Its queued microtasks run before the timer callback. Small logging and scheduling costs make measured timing approximate.

### Chunking does not choose a queue

```latex
W = 4 \times 20\,\mathrm{ms} = 80\,\mathrm{ms}
```

Awaiting a fulfilled promise splits the source into continuations but keeps them in the microtask checkpoint. Awaiting a timer gives another task an opportunity; its resolver then schedules the async continuation as a microtask.

## Transcript

### 0:00 Delay is a minimum

A ten-millisecond timer is a request to wait at least ten milliseconds, and this example shows why its callback still cannot run when that wait ends.

### 0:10 Schedule the work

The script prints start, schedules the timer, queues two microtasks, and then holds the thread for eighty milliseconds before printing end, with every line visible in TypeScript.

### 0:20 Run to completion

When our clock reaches ten, the timer becomes eligible in this model, but the current task keeps the thread until eighty, so becoming eligible never interrupts synchronous code.

### 0:31 The checkpoint

After the script returns, the microtask checkpoint runs A first, and the timer keeps waiting because the browser drains this queue before selecting another task.

### 0:41 New microtasks join

A now appends C behind B, so the same checkpoint runs B and then C, giving start, end, A, B, C, timer in both browsers we tested.

### 0:51 A promise still waits

Splitting the work into four chunks changes nothing if each continuation awaits an already fulfilled promise, because those continuations are microtasks and our timer still waits roughly eighty milliseconds.

### 1:02 A chance to run

Task-level yields create a chance to run between chunks: our two browsers ran the timer around forty and twenty milliseconds, showing why yielding improves responsiveness without promising an exact start time.

### 1:14 Three separate moments

Keep timer delay, queue eligibility, and callback execution separate, and for expensive work choose task-level yields or a worker, because a promise continuation alone does not give timer tasks a turn.

## Sources

- [WHATWG HTML: event loop processing model](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model)
- [WHATWG HTML: perform a microtask checkpoint](https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint)
- [WHATWG HTML: timers](https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers)
- [ECMAScript: jobs and run-to-completion](https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-jobs)
- [ECMAScript: await and promise continuations](https://tc39.es/ecma262/multipage/control-abstraction-objects.html#await)

## More visualisations

- [Distributed training: data, tensor and pipeline parallelism](https://understandingdata.com/ai-engineering-visualised/parallelism/)
- [Continuous batching](https://understandingdata.com/ai-engineering-visualised/continuous-batching/)

---

Source: https://understandingdata.com/ai-engineering-visualised/event-loop/
From AI Engineering Visualised by James Phoenix, Understanding Data. Published 2026-09-12.
