# Event Loop and Timers

Course: [Node.js](https://www.devtutorials.ai/learn/nodejs) · Section: Asynchronous Node.js
Source: https://www.devtutorials.ai/learn/nodejs/async/event-loop-and-timers

> How Node.js runs many jobs on one thread: blocking and non-blocking code, I/O-bound and CPU-bound work, the call stack, libuv and the thread pool, the event loop phases, timers, setImmediate, timers/promises, process.nextTick and the microtask queue, predicting output order, and how to avoid blocking the event loop.

## Event Loop and Timers

**Event loop** = the part of Node.js that picks the next job to run, again and again.

- Node.js runs your JavaScript on **one thread**. One thing happens at a time.
- Slow jobs, like reading a file, are handed to the operating system.
- When a slow job finishes, its callback waits in a queue.
- The event loop takes callbacks from the queues and runs them one by one.

**Simple lesson:** Node.js does one thing at a time, but it never waits around for slow jobs.

### A Real-World Example

Think of the school canteen at lunch time.

- One person stands at the counter.
- A student orders dosa. The counter person passes the order to the kitchen.
- She does not wait. She takes the next order.
- When a plate is ready, the kitchen rings a bell. She hands it over.

The counter person is the event loop. The kitchen is the operating system. The bell is a callback.

You already met the call stack, the microtask queue and the task queue in **Asynchronous Fundamentals** in the JavaScript course. This lesson shows the extra parts Node.js adds: libuv, the phases, `setImmediate` and `process.nextTick`.

## What You Will Learn

In this lesson, you will learn:

1. Single-threaded code, blocking and non-blocking work
2. I/O-bound and CPU-bound work
3. The call stack, libuv and the thread pool
4. The phases of the event loop
5. `setTimeout`, `setInterval`, `setImmediate` and `timers/promises`
6. `process.nextTick` and the microtask queue
7. How to predict the order of output
8. How to keep the event loop free

## Concurrency Model

**Blocking code** = code that makes everything else wait until it finishes.

**Non-blocking code** = code that starts a job and lets the program go on.

- `readFileSync` is blocking. The next line waits for the file.
- `readFile` with a callback is non-blocking. The next line runs at once.
- While a blocking call runs, Node.js cannot answer anyone else.

Click **▶ Run**.

- **Line 2** came before line 3, because the sync read finished first.
- **Line 5** came last, because its callback waited for the main code to end.

### I/O-Bound vs CPU-Bound Work

- **I/O-bound** = waiting for something outside: a file, a database, a website. Node.js is great at this.
- **CPU-bound** = hard thinking work: big maths, resizing photos, sorting millions of rows. This blocks the one thread.
- For CPU-bound work, see **Child Processes, Worker Threads and Clustering**.

**Simple lesson:** Node.js shines when it waits a lot, and struggles when it thinks a lot.

## Architecture: Call Stack, libuv and the Thread Pool

- **Call stack** = the list of functions running right now. V8 manages it.
- **libuv** = the C library that runs the event loop and talks to the operating system.
- **Thread pool** = 4 helper threads inside libuv for jobs the operating system cannot do without waiting.
- **`UV_THREADPOOL_SIZE`** = an environment variable that changes the pool size (default 4, up to 1024).

Jobs that use the thread pool:

- Most `fs` file operations
- `crypto.pbkdf2`, `crypto.scrypt` and other heavy `crypto` work
- `zlib` compression
- `dns.lookup`

Network sockets do **not** use the pool. The operating system tells libuv when data arrives.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 220" role="img" ariaLabelledby="fig-node-pool" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-pool">Animation: JavaScript on one thread hands file and crypto jobs to four thread pool workers in libuv, and the results come back as callbacks</title>
<rect x="20" y="80" width="150" height="60" rx="10"  style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }}/>
<text x="95" y="105" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Main thread</text>
<text x="95" y="124" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>your JavaScript</text>
<rect x="230" y="20" width="310" height="180" rx="12"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }}/>
<text x="385" y="42" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>libuv thread pool (4 workers)</text>
<rect x="250" y="60" width="130" height="28" rx="6"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/>
<text x="315" y="79" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 11 }}>worker 1: readFile</text>
<rect x="390" y="60" width="130" height="28" rx="6"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/>
<text x="455" y="79" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 11 }}>worker 2: pbkdf2</text>
<rect x="250" y="100" width="130" height="28" rx="6"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/>
<text x="315" y="119" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 11 }}>worker 3: zlib</text>
<rect x="390" y="100" width="130" height="28" rx="6"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/>
<text x="455" y="119" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 11 }}>worker 4: free</text>
<text x="385" y="160" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>A 5th job waits until a worker is free</text>
<circle r="6" cx="170" cy="110" style={{ fill: "var(--ngc-orange)" }}>
<animate attributeName="cx" values="170;250;250;170" keyTimes="0;0.35;0.65;1" dur="4s" repeatCount="indefinite" />
<animate attributeName="cy" values="110;74;74;110" keyTimes="0;0.35;0.65;1" dur="4s" repeatCount="indefinite" />
</circle>
<text x="95" y="170" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 11, fontWeight: 700 }}>callback comes back</text>
</svg>
<figcaption>JavaScript runs on one thread, while libuv's workers do slow file and crypto jobs.</figcaption>
</figure>

**Simple lesson:** your code is single-threaded, but libuv has helpers for slow jobs.

## Event Loop Phases

Each turn of the event loop visits its queues in a fixed order. One turn is called a **tick**.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 250" role="img" ariaLabelledby="fig-node-phases" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-phases">Animation: the event loop moves through the timers, pending callbacks, idle and prepare, poll, check and close callbacks phases in a circle</title>
<circle cx="200" cy="125" r="95"  style={{ fill: "none", stroke: "var(--ngc-slate)", strokeWidth: 2, strokeDasharray: "5 5" }}/>
<rect x="140" y="15" width="120" height="32" rx="8"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }}/><text x="200" y="36" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>1. timers</text>
<rect x="270" y="60" width="130" height="32" rx="8" opacity="0.12"  style={{ fill: "var(--ngc-slate)", stroke: "var(--ngc-slate)" }}/><text x="335" y="81" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>2. pending callbacks</text>
<rect x="270" y="160" width="130" height="32" rx="8" opacity="0.12"  style={{ fill: "var(--ngc-slate)", stroke: "var(--ngc-slate)" }}/><text x="335" y="181" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>3. idle, prepare</text>
<rect x="140" y="205" width="120" height="32" rx="8"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }}/><text x="200" y="226" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>4. poll</text>
<rect x="10" y="160" width="110" height="32" rx="8"  style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }}/><text x="65" y="181" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>5. check</text>
<rect x="10" y="60" width="110" height="32" rx="8" opacity="0.12"  style={{ fill: "var(--ngc-slate)", stroke: "var(--ngc-slate)" }}/><text x="65" y="81" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>6. close</text>
<circle r="8" cx="200" cy="30" style={{ fill: "var(--ngc-orange)" }}>
<animateMotion dur="8s" repeatCount="indefinite" path="M 0 0 A 95 95 0 1 1 -0.1 0" />
</circle>
<text x="420" y="30" textAnchor="start" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>timers:</text>
<text x="420" y="46" textAnchor="start" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>setTimeout, setInterval</text>
<text x="420" y="92" textAnchor="start" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>poll:</text>
<text x="420" y="108" textAnchor="start" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>file and network callbacks</text>
<text x="420" y="154" textAnchor="start" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>check:</text>
<text x="420" y="170" textAnchor="start" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>setImmediate</text>
<text x="420" y="214" textAnchor="start" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 11, fontWeight: 700 }}>Between every callback:</text>
<text x="420" y="230" textAnchor="start" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>nextTick, then promises</text>
</svg>
<figcaption>The loop goes round the phases. Small queues run between every callback.</figcaption>
</figure>

The three phases you will use most:

- **Timers** = runs `setTimeout` and `setInterval` callbacks whose time is up.
- **Poll** = waits for and runs I/O callbacks, such as a finished file read.
- **Check** = runs `setImmediate` callbacks, right after poll.

The other phases (advanced):

- **Pending callbacks** = some system errors from the last turn, such as a refused TCP connection.
- **Idle, prepare** = used inside Node.js only.
- **Close callbacks** = `close` events, such as `socket.on("close")`.

When every queue is empty and nothing is waiting, the loop stops and the program ends.

**Simple lesson:** timers, then I/O, then `setImmediate`, round and round until nothing is left.

## Timers

- **`setTimeout(fn, ms)`** = run `fn` once, after at least `ms` milliseconds.
- **`setInterval(fn, ms)`** = run `fn` again and again, every `ms` milliseconds.
- **`clearTimeout(id)` and `clearInterval(id)`** = cancel them.
- **`setImmediate(fn)`** = run `fn` in the check phase, after the current I/O.
- **Timer accuracy:** `ms` is a minimum, not a promise. A busy program runs the timer late.

### timers/promises

`node:timers/promises` gives promise versions of the timers. They work with `await`.

### ref() and unref()

- A waiting timer keeps the program alive.
- `timer.unref()` = do not keep the program alive just for this timer.
- `timer.ref()` = undo that.

Use `timers/promises` with `await` instead of nesting `setTimeout` callbacks. The code then reads from top to bottom.

```js
// main.mjs
const reminder = setInterval(() => console.log("tick"), 1000);
reminder.unref(); // the program may end even though the interval exists
console.log("Main code done. The program ends without waiting.");
```

Output:

```text
Main code done. The program ends without waiting.
```

**Simple lesson:** timers wait at least the time you ask, and `await` on a timer pauses only your function.

## Task Queues: nextTick and Microtasks

Two small queues run **between** callbacks, before the event loop moves on.

- **`process.nextTick` queue** = runs first, as soon as the current code finishes.
- **Microtask queue** = promise callbacks (`then`, `await`) and `queueMicrotask`. Runs right after the nextTick queue.
- Only then does the loop go to timers, I/O and `setImmediate`.

Click **▶ Run**.

- **Sync code** always finishes first.
- **nextTick** jumped ahead of the promise, even though it was written later.
- **Promise and queueMicrotask** run in the order they were added.
- **setTimeout** waited for the next turn of the loop.

### Starving the Event Loop

- If a `nextTick` callback adds another `nextTick`, forever, the loop never moves on.
- Timers and I/O never run. This is called **starvation**.
- Prefer `queueMicrotask` or `setImmediate` for repeated work.

A `while` loop that waits for a timer never ends. The timer cannot run while the loop holds the thread.

**Simple lesson:** sync, then nextTick, then promises, then the event loop phases.

## Execution Order

### Ordering Inside I/O Callbacks vs the Main Module

- In the **main module**, `setTimeout(fn, 0)` and `setImmediate(fn)` can run in either order. It depends on how busy the computer is.
- **Inside an I/O callback**, `setImmediate` always runs first. The check phase comes right after poll.

```js
// main.mjs
import { readFile } from "node:fs";

readFile(import.meta.filename, () => {
  // We are in the poll phase now.
  setTimeout(() => console.log("timeout"), 0);
  setImmediate(() => console.log("immediate"));
});
```

Output:

```text
immediate
timeout
```

### Predicting Output Order

Use these steps:

1. Run all sync code from top to bottom.
2. Run all `nextTick` callbacks.
3. Run all promise callbacks.
4. Go to the next phase of the loop. Repeat steps 2 and 3 after every callback.

**Simple lesson:** inside I/O callbacks, `setImmediate` beats `setTimeout`.

## Blocking the Event Loop

Common blocking operations:

- `readFileSync` and other `...Sync` calls on big files
- Long `for` loops over huge arrays
- `JSON.parse` of a very large text
- Heavy maths, such as checking primes up to millions
- A badly written regular expression on long text

### Splitting Work with setImmediate

- Cut a long job into small pieces.
- Do one piece, then schedule the next with `setImmediate`.
- Between pieces, the event loop can run timers and I/O.

**Simple lesson:** never hold the one thread for long. Cut big work into small pieces.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Using `readFileSync` in a busy server | Sync is simpler and just as fast | Use `fs/promises` or callbacks | Every user waits while one file is read |
| Forgetting `clearInterval` | The interval stops by itself | Keep the id and clear it | The program never ends |
| Trusting `setTimeout(fn, 100)` to be exact | Timers are precise | The delay is a minimum | Code runs late when the loop is busy |
| Calling `nextTick` inside `nextTick` forever | It is just a fast `setTimeout` | It runs before the loop moves on | Timers and I/O starve |
| Long loops in a request handler | One loop is harmless | Split the work or use a worker | All other users freeze |
| Expecting a fixed order of `setTimeout 0` and `setImmediate` in main code | One always wins | It can change between runs | Flaky output and tests |

</div>

## TL;DR

1. **Event loop** = picks the next callback to run, one at a time.
2. **Blocking** = everything waits. **Non-blocking** = start the job and move on.
3. **libuv** = runs the loop and the **thread pool** (4 workers by default).
4. **Phases** = timers, pending, idle/prepare, poll, check, close.
5. **setTimeout** = timers phase. **setImmediate** = check phase.
6. **Order** = sync code, then nextTick, then promises, then the next phase.
7. **Starvation** = endless nextTick work stops the loop.
8. **Split long work** with `setImmediate`, or move it to a worker thread.

## Check Yourself

## Exercises

## What to Remember

- Node.js runs JavaScript on one thread and never waits for slow jobs.
- libuv runs the event loop and a small thread pool.
- The loop visits timers, poll (I/O) and check (`setImmediate`), among others.
- Sync code runs first, then nextTick, then promises, then the next phase.
- Timers wait at least the time you ask, never less.
- Split long work, or the whole program freezes.

Next: **Events and Utilities**. You will make your own events and use the `util` toolbox.
