Search

What Is the Event Loop and Why Is Node.js Single-Threaded?

The short answer

Quick answer: The event loop is a loop that repeatedly takes the next waiting task and runs it to completion. JavaScript runs your code on a single thread with one call stack, so only one piece of code executes at a time. Slow operations such as network requests are handed off to the browser or operating system, and when they finish, a callback is placed in a queue. Whenever the call stack is empty, the event loop takes the next callback from the queue and runs it. Node.js is "single-threaded" in the sense that your JavaScript runs on one thread, while I/O happens in the background.

The pieces

PieceRole
Call stackThe function currently running, and the functions that called it
Runtime APIsTimers, network, file access: provided by the browser or Node.js, not by JavaScript itself
Task queueCallbacks waiting to run (timers, I/O, user events)
Microtask queueHigher-priority callbacks (promise reactions, queueMicrotask)
Event loopMoves callbacks from the queues to the stack when the stack is empty

The key rule is run to completion: once a function starts, nothing else can interrupt it until the stack is empty again.

A worked example

console.log("1: start");

setTimeout(() => console.log("2: timeout"), 0);

Promise.resolve().then(() => console.log("3: promise"));

console.log("4: end");

The output is:

1: start
4: end
3: promise
2: timeout

Here is why:

  1. "1: start" is logged immediately.
  2. setTimeout hands a callback to the runtime's timer. Even with a delay of 0, it goes to the task queue.
  3. The resolved promise's .then callback goes to the microtask queue.
  4. "4: end" is logged. The script finishes and the stack is empty.
  5. The event loop drains the whole microtask queue first: "3: promise".
  6. Then it takes the next task: "2: timeout".

Microtasks vs tasks

After each task, and before moving to the next one, the loop runs every pending microtask, including any that are added while it is doing so.

Tasks (macrotasks)Microtasks
SourcessetTimeout, setInterval, I/O callbacks, UI eventsPromise .then/catch/finally, await continuations, queueMicrotask
When they runOne per loop turnAll of them, after each task
RiskSlow tasks delay everythingAn endless chain of microtasks starves the loop

This is why code after an await runs before a zero-delay timer. The mechanics of await are covered in how async/await works.

Why JavaScript was built this way

JavaScript was created to script web pages, where code manipulates one shared document. With multiple threads changing the same page at once, every script would need locks and would risk race conditions. A single thread removes that whole class of bugs: your code never gets interrupted halfway through updating something.

In browsers, the same thread also handles layout and painting. That is why a long-running script freezes the page: the browser cannot render or respond to clicks until the stack is empty.

How Node.js serves thousands of connections on one thread

The trick is that waiting does not need a thread. When Node.js starts a network read, it asks the operating system to notify it when data arrives, then goes back to the loop. One thread can track tens of thousands of sockets this way, because at any moment almost all of them are idle.

Under the hood, Node.js uses a library called libuv:

  • Network I/O uses the operating system's own notification mechanisms (epoll on Linux, kqueue on macOS, IOCP on Windows).
  • File system operations, DNS lookups and some cryptography have no good non-blocking interface on every platform, so libuv runs them on a small thread pool (four threads by default).

So Node.js is not literally single-threaded. Your JavaScript is; the plumbing is not.

The Node.js loop runs in phases on each turn, as the official guide describes:

  1. Timers: setTimeout and setInterval callbacks that are due.
  2. Pending callbacks: some deferred system callbacks.
  3. Poll: retrieve new I/O events and run their callbacks; wait here if there is nothing else to do.
  4. Check: setImmediate callbacks.
  5. Close callbacks: such as a socket's close event.

Between callbacks, Node.js runs process.nextTick callbacks and then promise microtasks.

Compared with a thread per request

Thread per requestEvent loop
Memory per connectionA thread stack eachA small object each
Many idle connectionsExpensiveCheap
CPU-heavy requestUses its own thread; others unaffectedBlocks everyone
Shared stateNeeds locksNo locks within one thread
Mental modelStraight-line codeCallbacks, promises, async/await

That is why event-driven servers suit chat, APIs and WebSockets, where connections are numerous and mostly waiting. The trade-offs with threads are discussed in processes vs threads.

Do not block the loop

Because everything shares one thread, a single slow function delays every other user. Common culprits:

  • Large synchronous loops or heavy calculations.
  • JSON.parse or JSON.stringify on huge payloads.
  • Synchronous APIs such as fs.readFileSync in request handlers.
  • Regular expressions with catastrophic backtracking; see how regular expressions work.

Ways to fix it:

  • Move CPU work off the thread. Use Worker Threads in Node.js or Web Workers in the browser.
  • Split the work. Process in chunks and yield between them.
  • Scale out. Run one Node.js process per core behind a load balancer.
  • Measure. Track event loop delay to catch blocking early.

A note on timers: setTimeout(fn, 100) means "no sooner than 100 ms". If the loop is busy, the callback runs late.

Other languages have event loops too

The pattern is not unique to JavaScript. Python's asyncio, Rust's Tokio, Nginx, Redis and every desktop GUI toolkit are built around an event loop. The details differ, but the shape is the same: wait for events, run a handler, repeat.

Frequently asked questions

Is Node.js really single-threaded?

Your JavaScript runs on one thread. Node.js itself uses additional threads for some I/O, and you can create Worker Threads for CPU-heavy work.

Why does setTimeout with 0 not run immediately?

Its callback is queued as a task and can only run after the current code finishes and all microtasks are drained.

What is a microtask?

A small callback, usually from a promise, that runs right after the current task and before the next one.

What happens if I run an infinite loop?

Nothing else can run: no timers, no I/O callbacks, no rendering. The program or page appears frozen.

Conclusion

The event loop is a simple idea with large consequences: one thread, one stack, and queues of callbacks run one at a time. It makes concurrency easy to reason about and lets one process juggle huge numbers of connections, provided no single task keeps the thread for long.

Related articles

Sources and further reading

Usama Muneer

Usama Muneer

Coder, Blogger, Tech Speaker & Web Technologies Enthusiast. Passionate about working on open-source Programming languages & Tools while utilizing my Product Development skills.

Your experience on this site will be improved by allowing cookies Cookie Policy