What is the event loop in Node.js?
- tool
- Node.js
- versions
- Node.js 20 – 26
- os
- any
The event loop is the mechanism that lets Node.js run JavaScript on a single thread and still handle many I/O operations at once. It takes finished operations (a timer that expired, a file read that completed, a request that arrived) and runs their callbacks one at a time. If the loop has nothing left to wait for, the process exits. The description below follows the official guide The Node.js Event Loop.
Phases
Each pass through the loop visits phases in a fixed order, and every phase has a first-in-first-out queue of callbacks:
| Phase | What runs there |
|---|---|
| timers | Callbacks from setTimeout() and setInterval() whose time has passed |
| pending callbacks | Some system callbacks deferred to the next iteration, such as certain TCP errors |
| idle, prepare | Used internally only |
| poll | Retrieves new I/O events and runs their callbacks; Node.js can block here while it waits |
| check | setImmediate() callbacks |
| close callbacks | Some close events, for example socket.destroy() |
Between phases, Node.js also runs process.nextTick() callbacks and resolved promise callbacks (the microtask queue) before moving on. According to the guide, starting with libuv 1.45.0 (Node.js 20), timers run after the poll phase in each iteration; earlier versions ran them before polling.
Example
cat > loop.js <<'EOF'
const fs = require('node:fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
EOF
node loop.jsOutput
sync nextTick promise immediate timeout
The synchronous line prints first. Then nextTick and the promise callback run before the loop continues. Inside the I/O callback, setImmediate is guaranteed to run before a zero-delay timeout, because the loop moves from poll straight to check.
setTimeout, setImmediate and nextTick
process.nextTick()runs right after the current operation, before the loop continues. Recursive use can starve I/O.setImmediate()runs in the check phase of the current iteration, after poll.setTimeout(fn, ms)runs in the timers phase once at leastmshave passed; it is a threshold, not an exact time.
Practical consequences
- Never block the loop. A long synchronous computation delays every other callback, timers included. Split the work or use a worker thread.
- Timers are late, not early. A 100 ms timer fires after 100 ms or more, depending on what else is running.
- Memory problems look like freezes. When the process is near its heap limit, garbage collection pauses the loop; see increase the memory limit.
- The process ends when the loop is empty. The exit status is described in Node.js exit codes.
FAQ
Is Node.js single-threaded?
Your JavaScript runs on one thread by default, but Node.js hands I/O to the operating system kernel and libuv, which can use other threads internally. The event loop is what brings the results back to your single JavaScript thread as callbacks.
What is the difference between setImmediate and setTimeout 0?
setImmediate runs in the check phase right after poll. A zero-delay setTimeout runs in the timers phase once its threshold has passed. From the main module the order can vary; from inside an I/O callback setImmediate always runs first.
Why does a long loop freeze my Node.js server?
Callbacks run one at a time. While your code occupies the thread, the loop cannot reach the poll phase, so no new connections or timers are handled. Split heavy work into chunks, or move it to a worker thread or another process.