Event Loop and Timers
By Syncfusion®, Inc. · Last updated
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:
- Single-threaded code, blocking and non-blocking work
- I/O-bound and CPU-bound work
- The call stack, libuv and the thread pool
- The phases of the event loop
setTimeout,setInterval,setImmediateandtimers/promisesprocess.nextTickand the microtask queue- How to predict the order of output
- 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.
readFileSyncis blocking. The next line waits for the file.readFilewith 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
fsfile operations crypto.pbkdf2,crypto.scryptand other heavycryptoworkzlibcompressiondns.lookup
Network sockets do not use the pool. The operating system tells libuv when data arrives.
Animation: JavaScript on one thread hands file and crypto jobs to four thread pool workers in libuv, and the results come back as callbacks
Main thread your JavaScript
libuv thread pool (4 workers)
worker 1: readFile
worker 2: pbkdf2
worker 3: zlib
worker 4: free A 5th job waits until a worker is free
callback comes back
JavaScript runs on one thread, while libuv's workers do slow file and crypto jobs.
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.
Animation: the event loop moves through the timers, pending callbacks, idle and prepare, poll, check and close callbacks phases in a circle
1. timers 2. pending callbacks 3. idle, prepare 4. poll 5. check 6. close
timers: setTimeout, setInterval poll: file and network callbacks check: setImmediate Between every callback: nextTick, then promises
The loop goes round the phases. Small queues run between every callback.
The three phases you will use most:
- Timers = runs
setTimeoutandsetIntervalcallbacks whose time is up. - Poll = waits for and runs I/O callbacks, such as a finished file read.
- Check = runs
setImmediatecallbacks, 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 =
closeevents, such assocket.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)= runfnonce, after at leastmsmilliseconds.setInterval(fn, ms)= runfnagain and again, everymsmilliseconds.clearTimeout(id)andclearInterval(id)= cancel them.setImmediate(fn)= runfnin the check phase, after the current I/O.- Timer accuracy:
msis 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.
// 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:
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.nextTickqueue = runs first, as soon as the current code finishes.- Microtask queue = promise callbacks (
then,await) andqueueMicrotask. 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
nextTickcallback adds anothernextTick, forever, the loop never moves on. - Timers and I/O never run. This is called starvation.
- Prefer
queueMicrotaskorsetImmediatefor 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)andsetImmediate(fn)can run in either order. It depends on how busy the computer is. - Inside an I/O callback,
setImmediatealways runs first. The check phase comes right after poll.
// 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:
immediate
timeout
Predicting Output Order
Use these steps:
- Run all sync code from top to bottom.
- Run all
nextTickcallbacks. - Run all promise callbacks.
- 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:
readFileSyncand other...Synccalls on big files- Long
forloops over huge arrays JSON.parseof 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
| 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 |
TL;DR
- Event loop = picks the next callback to run, one at a time.
- Blocking = everything waits. Non-blocking = start the job and move on.
- libuv = runs the loop and the thread pool (4 workers by default).
- Phases = timers, pending, idle/prepare, poll, check, close.
- setTimeout = timers phase. setImmediate = check phase.
- Order = sync code, then nextTick, then promises, then the next phase.
- Starvation = endless nextTick work stops the loop.
- 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.